DataStatistics数据显示页面---test.vue中测试全局弹窗提示
This commit is contained in:
59
package-lock.json
generated
59
package-lock.json
generated
@@ -14,7 +14,12 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.5.1",
|
||||
"element-plus": "^2.7.8",
|
||||
"file-saver": "^2.0.5",
|
||||
"jsmpeg": "^1.0.0",
|
||||
"json2csv": "^5.0.7",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"vue": "^3.4.29",
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
@@ -1189,6 +1194,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/commander/-/commander-6.2.1.tgz",
|
||||
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/computeds": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz",
|
||||
@@ -1351,6 +1364,11 @@
|
||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||
},
|
||||
"node_modules/file-saver": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz",
|
||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
@@ -1465,6 +1483,32 @@
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json2csv": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/json2csv/-/json2csv-5.0.7.tgz",
|
||||
"integrity": "sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"dependencies": {
|
||||
"commander": "^6.1.0",
|
||||
"jsonparse": "^1.3.1",
|
||||
"lodash.get": "^4.4.2"
|
||||
},
|
||||
"bin": {
|
||||
"json2csv": "bin/json2csv.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10",
|
||||
"npm": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonparse": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/jsonparse/-/jsonparse-1.3.1.tgz",
|
||||
"integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
|
||||
"engines": [
|
||||
"node >= 0.2.0"
|
||||
]
|
||||
},
|
||||
"node_modules/less": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/less/-/less-4.2.0.tgz",
|
||||
@@ -1537,6 +1581,11 @@
|
||||
"lodash-es": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.11",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.11.tgz",
|
||||
@@ -1630,6 +1679,11 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
|
||||
},
|
||||
"node_modules/muggle-string": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||
@@ -1709,6 +1763,11 @@
|
||||
"npm": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/papaparse": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/papaparse/-/papaparse-5.4.1.tgz",
|
||||
"integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw=="
|
||||
},
|
||||
"node_modules/parse-node-version": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz",
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.5.1",
|
||||
"element-plus": "^2.7.8",
|
||||
"file-saver": "^2.0.5",
|
||||
"jsmpeg": "^1.0.0",
|
||||
"json2csv": "^5.0.7",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"vue": "^3.4.29",
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
|
||||
94
src/App.vue
94
src/App.vue
@@ -1,26 +1,102 @@
|
||||
<template>
|
||||
<div id="app" class="app-container">
|
||||
<router-view></router-view>
|
||||
<router-view></router-view>
|
||||
<el-dialog
|
||||
title="告警详情"
|
||||
v-model="globalDialogVisible"
|
||||
width="50%"
|
||||
@close="handleDialogClose">
|
||||
<el-row :gutter="30" style="margin-bottom: 2vh;">
|
||||
<el-col span="8">告警编号:{{ globalDialogContent.id }}</el-col>
|
||||
<el-col span="8">摄像头名称:{{ globalDialogContent.camera?.name }}</el-col>
|
||||
<el-col span="8">告警类型:{{ algorithmMap.get(globalDialogContent.types) || '未知类型' }}</el-col>
|
||||
</el-row>
|
||||
<img :src="globalDialogContent.snapshotUrl" alt="告警图片" v-if="globalDialogContent.snapshotUrl" style="max-width: 100%;" />
|
||||
<video v-if="globalDialogContent.videoUrl" :src="globalDialogContent.videoUrl" controls style="max-width: 100%;"></video>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { provide } from 'vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
import { BoxApi } from '@/utils/boxApi';
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
setup() {
|
||||
const globalDialogVisible = ref(false);
|
||||
const globalDialogContent = ref({
|
||||
id: null,
|
||||
camera_id: null,
|
||||
camera: { name: '' },
|
||||
types: null,
|
||||
started_at: null,
|
||||
snapshotUrl: '',
|
||||
videoUrl: ''
|
||||
});
|
||||
|
||||
const algorithmMap = ref(new Map());
|
||||
|
||||
const loadAlgorithms = async () => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const algorithms = await apiInstance.getAlgorithms(token);
|
||||
algorithmMap.value = new Map(algorithms.map((algo: { code_name: string, name: string }) => [algo.code_name, algo.name]));
|
||||
};
|
||||
|
||||
const showDialog = async (data: any) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const eventDetails = await apiInstance.getEventById(data.id, token);
|
||||
const snapshot = eventDetails.mediums.find((item: any) => item.name === 'snapshot');
|
||||
const video = eventDetails.mediums.find((item: any) => item.name === 'video');
|
||||
|
||||
globalDialogContent.value = {
|
||||
id: eventDetails.id,
|
||||
camera_id: eventDetails.camera_id,
|
||||
camera: eventDetails.camera,
|
||||
types: eventDetails.types,
|
||||
started_at: eventDetails.started_at,
|
||||
snapshotUrl: snapshot?.file || '',
|
||||
videoUrl: video?.file || ''
|
||||
};
|
||||
|
||||
globalDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDialogClose = () => {
|
||||
globalDialogVisible.value = false;
|
||||
globalDialogContent.value = {
|
||||
id: null,
|
||||
camera_id: null,
|
||||
camera: { name: '' },
|
||||
types: null,
|
||||
started_at: null,
|
||||
snapshotUrl: '',
|
||||
videoUrl: ''
|
||||
};
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadAlgorithms();
|
||||
eventBus.on('showDialog', showDialog);
|
||||
});
|
||||
|
||||
return {
|
||||
globalDialogVisible,
|
||||
globalDialogContent,
|
||||
handleDialogClose,
|
||||
algorithmMap
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#app{
|
||||
#app {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/* .app-container{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
} */
|
||||
</style>
|
||||
</style>
|
||||
|
||||
0
src/bk/DataStatics新数据统计.vue
Normal file
0
src/bk/DataStatics新数据统计.vue
Normal file
127
src/bk/DataStatistics原始版.vue
Normal file
127
src/bk/DataStatistics原始版.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<!-- 左上角的点播不要,布局调整,整体布局改动需求,原版备份(2024.11.12) -->
|
||||
|
||||
<template>
|
||||
<div class="alert-container" >
|
||||
<el-row class="bottom-pan">
|
||||
<el-col class="panel-bottom">
|
||||
<Cameras/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row class="top-pan">
|
||||
<el-col :sm="24" :md="12" class="panel-top-left">
|
||||
<statistics />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" class="panel-top-right">
|
||||
<alertChart />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted,computed, onBeforeUnmount} from 'vue';
|
||||
import Statistics from '@/components/Statistics.vue';
|
||||
import AlertChart from '@/components/AlertChart.vue';
|
||||
import Cameras from '@/components/Cameras.vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
||||
|
||||
// 创建 BoxApi 实例
|
||||
const boxApi = new BoxApi();
|
||||
const typeMapping = reactive({});
|
||||
const token = ref(null);
|
||||
// const scale = ref(1);
|
||||
|
||||
// const scaleStyle = computed(() => ({
|
||||
// transform: `scale(${scale.value})`,
|
||||
// transformOrigin: 'top left',
|
||||
// width: `${100 / scale.value}%`,
|
||||
// }));
|
||||
|
||||
// const handleResize = () => {
|
||||
// const clientWidth = document.documentElement.clientWidth;
|
||||
// const scaleFactor = clientWidth / 1920;
|
||||
// scale.value = scaleFactor < 1 ? scaleFactor : 1;
|
||||
// };
|
||||
|
||||
// 获取类型映射
|
||||
const fetchTypeMapping = async (token) => {
|
||||
try {
|
||||
const algorithms = await boxApi.getAlgorithms(token); // 使用 BoxApi 的 getAlgorithms 方法
|
||||
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
||||
algorithms.forEach((algorithm) => {
|
||||
typeMapping[algorithm.code_name] = algorithm.name;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching algorithms:", error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
token.value = localStorage.getItem('alertToken');
|
||||
await fetchTypeMapping(token.value);
|
||||
// handleResize();
|
||||
// window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// onBeforeUnmount(() => {
|
||||
// window.removeEventListener('resize', handleResize);
|
||||
// });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alert-container {
|
||||
/* transform: scale(0.97); */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f5f7fa;
|
||||
overflow-x: hidden;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.top-pan {
|
||||
/* padding: 10px; */
|
||||
/* margin-bottom: 10px; */
|
||||
/* display: flex; */
|
||||
/* gap: 5px; */
|
||||
/* background-color: #fff; */
|
||||
/* background-color: #1E2E4A; */
|
||||
/* overflow: hidden; */
|
||||
/* height: 55vh; */
|
||||
/* max-height: 450px; */
|
||||
/* padding-left: 1vh; */
|
||||
/* padding-right:1vh ; */
|
||||
/* overflow: hidden; */
|
||||
height: 56vh;
|
||||
margin-top: 60px;
|
||||
/* border: 1px solid #1E2E4A; */
|
||||
|
||||
}
|
||||
.bottom-pan{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 33vh;
|
||||
}
|
||||
|
||||
.panel-top-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-top-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* gap: 20px; */
|
||||
}
|
||||
|
||||
.panel-bottom{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
410
src/bk/告警数量分布问题代码.vue
Normal file
410
src/bk/告警数量分布问题代码.vue
Normal file
@@ -0,0 +1,410 @@
|
||||
<!-- 问题描述:今日数据加载成功,切换到本周数据加载完毕后在切换回今日数据,今日数据异常,缓存问题,数组对应错误 -->
|
||||
|
||||
<template>
|
||||
<div class="alert-container">
|
||||
<!-- <el-card class="alert-card">
|
||||
<div class="alert-header">告警趋势</div>
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
||||
<el-tab-pane label="今日" name="first">
|
||||
<div ref="todayLineChartDom" class="chart-container"></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="本周" name="second">
|
||||
<div ref="weekLineChartDom" class="chart-container"></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="最近30天" name="third">
|
||||
<div ref="monthLineChartDom" class="chart-container"></div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card> -->
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
||||
<el-tab-pane label="今日" name="first">
|
||||
<div ref="todayLineChartDom" class="chart-container"></div> <!-- 今日图表 -->
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="本周" name="second">
|
||||
<div ref="weekLineChartDom" class="chart-container"></div> <!-- 本周图表 -->
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="最近30天" name="third"> <!-- 新增最近30天选项卡 -->
|
||||
<div ref="monthLineChartDom" class="chart-container"></div> <!-- 最近30天图表 -->
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import dayjs from 'dayjs';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const activeName = ref('first');
|
||||
const hourlyCounts = ref(Array(24).fill(0));
|
||||
const dailyCounts = ref(Array(7).fill(0));
|
||||
const monthlyCounts = ref(Array(30).fill(0));
|
||||
|
||||
const todayLineChartDom = ref(null);
|
||||
const weekLineChartDom = ref(null);
|
||||
const monthLineChartDom = ref(null);
|
||||
|
||||
const chartInstanceToday = ref(null);
|
||||
const chartInstanceWeek = ref(null);
|
||||
const chartInstanceMonth = ref(null);
|
||||
|
||||
const weekDates = ref([]);
|
||||
const monthDates = ref([]);
|
||||
|
||||
let isTodayChartInitialized = false;
|
||||
let isWeekChartInitialized = false;
|
||||
let isMonthChartInitialized = false;
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const allEvents = ref([]);
|
||||
|
||||
// 日期格式化函数
|
||||
const formatDateTime = (date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
// 获取时间参数
|
||||
const getDayParams = () => {
|
||||
const today = dayjs();
|
||||
return { timeAfter: today.startOf('day').format(), timeBefore: today.endOf('day').format() };
|
||||
};
|
||||
const getWeeklyParams = () => {
|
||||
const today = dayjs();
|
||||
return { timeAfter: today.subtract(7, 'day').format(), timeBefore: today.endOf('day').format() };
|
||||
};
|
||||
const getMonthlyParams = () => {
|
||||
const today = dayjs();
|
||||
return { timeAfter: today.subtract(30, 'day').format(), timeBefore: today.endOf('day').format() };
|
||||
};
|
||||
|
||||
// 计算日期
|
||||
const calculateWeekDates = () => {
|
||||
weekDates.value = Array.from({ length: 7 }, (_, i) => dayjs().subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
const calculateMonthDates = () => {
|
||||
monthDates.value = Array.from({ length: 30 }, (_, i) => dayjs().subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
|
||||
// 延迟初始化图表函数
|
||||
const delayedInitChart = debounce((chartInstance, domRef, option) => {
|
||||
nextTick(() => {
|
||||
if (domRef.value) {
|
||||
chartInstance.value = echarts.init(domRef.value);
|
||||
chartInstance.value.setOption(option);
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
|
||||
// 初始化图表选项配置
|
||||
const createTodayChartOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
console.log('Tooltip params:', params);
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`; // 使用 <strong> 标签作为小标题
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic', // 字体样式,可以是 'normal', 'italic', 'oblique'
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif', // 字体
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: Array.from({ length: 24 }, (_, i) => `${i}:00`),
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: hourlyCounts.value,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createWeekChartOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
console.log('Tooltip params:', params);
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`; // 使用 <strong> 标签作为小标题
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic', // 字体样式,可以是 'normal', 'italic', 'oblique'
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif', // 字体
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: weekDates.value,
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: dailyCounts.value,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createMonthChartOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
console.log('Tooltip params:', params);
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`; // 使用 <strong> 标签作为小标题
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic', // 字体样式,可以是 'normal', 'italic', 'oblique'
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif', // 字体
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: monthDates.value,
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: monthlyCounts.value,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 获取和处理事件数据
|
||||
const fetchAndProcessEvents = async (timeParams) => {
|
||||
try {
|
||||
let currentPage = 1;
|
||||
const pageSize = 1000;
|
||||
const token = localStorage.getItem('alertToken');
|
||||
allEvents.value = [];
|
||||
|
||||
const firstResponse = await apiInstance.getEventsByParams(token, pageSize, currentPage, timeParams.timeBefore, timeParams.timeAfter);
|
||||
const totalItems = firstResponse.count;
|
||||
allEvents.value.push(...firstResponse.results);
|
||||
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
while (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
const response = await apiInstance.getEventsByParams(token, pageSize, currentPage, timeParams.timeBefore, timeParams.timeAfter);
|
||||
allEvents.value.push(...response.results);
|
||||
}
|
||||
|
||||
processEventData(allEvents.value);
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理事件数据
|
||||
const processEventData = (events) => {
|
||||
hourlyCounts.value.fill(0);
|
||||
dailyCounts.value.fill(0);
|
||||
monthlyCounts.value.fill(0);
|
||||
events.forEach((event) => {
|
||||
const hour = dayjs(event.ended_at).hour();
|
||||
const dayDiff = dayjs().diff(dayjs(event.ended_at), 'day');
|
||||
if (dayDiff < 1) hourlyCounts.value[hour] += 1;
|
||||
if (dayDiff < 7) dailyCounts.value[6 - dayDiff] += 1;
|
||||
if (dayDiff < 30) monthlyCounts.value[29 - dayDiff] += 1;
|
||||
});
|
||||
updateCharts();
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateCharts = () => {
|
||||
if (chartInstanceToday.value) chartInstanceToday.value.setOption({ series: [{ data: hourlyCounts.value }] });
|
||||
if (chartInstanceWeek.value) chartInstanceWeek.value.setOption({ series: [{ data: dailyCounts.value }] });
|
||||
if (chartInstanceMonth.value) chartInstanceMonth.value.setOption({ series: [{ data: monthlyCounts.value }] });
|
||||
};
|
||||
|
||||
// 处理选项卡切换
|
||||
const handleClick = async (tab) => {
|
||||
let timeParams;
|
||||
if (tab.props.name === 'first' && !isTodayChartInitialized) {
|
||||
timeParams = getDayParams();
|
||||
await fetchAndProcessEvents(timeParams);
|
||||
delayedInitChart(chartInstanceToday, todayLineChartDom, createTodayChartOption());
|
||||
isTodayChartInitialized = true;
|
||||
} else if (tab.props.name === 'second' && !isWeekChartInitialized) {
|
||||
timeParams = getWeeklyParams();
|
||||
await fetchAndProcessEvents(timeParams);
|
||||
delayedInitChart(chartInstanceWeek, weekLineChartDom, createWeekChartOption());
|
||||
isWeekChartInitialized = true;
|
||||
} else if (tab.props.name === 'third' && !isMonthChartInitialized) {
|
||||
timeParams = getMonthlyParams();
|
||||
await fetchAndProcessEvents(timeParams);
|
||||
delayedInitChart(chartInstanceMonth, monthLineChartDom, createMonthChartOption());
|
||||
isMonthChartInitialized = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口调整时重新渲染图表
|
||||
const resizeCharts = debounce(() => {
|
||||
if (chartInstanceToday.value) chartInstanceToday.value.resize();
|
||||
if (chartInstanceWeek.value) chartInstanceWeek.value.resize();
|
||||
if (chartInstanceMonth.value) chartInstanceMonth.value.resize();
|
||||
}, 300);
|
||||
|
||||
// 组件挂载时调用
|
||||
onMounted(async () => {
|
||||
calculateWeekDates();
|
||||
calculateMonthDates();
|
||||
await handleClick({ props: { name: 'first' } });
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
.alert-card {
|
||||
background-color: #001529;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
/* padding: 10px; */
|
||||
/* margin: 10px; */
|
||||
}
|
||||
|
||||
.alert-container {
|
||||
background-color: #001529;
|
||||
margin: 0vh 1vw 1vh 1vw;
|
||||
/* margin-top: 0; */
|
||||
}
|
||||
|
||||
/* .alert-header {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #3a4b5c;
|
||||
} */
|
||||
|
||||
.chart-container {
|
||||
/* min-height: 350px; */
|
||||
width: 100%;
|
||||
height: 15vh;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
margin-left: 1vh;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: #2ea0ec;
|
||||
}
|
||||
|
||||
/* ::v-deep .el-tabs___nav-wrap.is-top::after{
|
||||
border: none;
|
||||
} */
|
||||
|
||||
/* ::v-deep .el-tabs__active-bar.is-top{
|
||||
padding: 0 204px;
|
||||
box-sizing: border-box !important;
|
||||
background-clip: content-box !important;
|
||||
} */
|
||||
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
/* width: 15vw; */
|
||||
position: static !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
514
src/bk/时间段告警数量分布.vue
Normal file
514
src/bk/时间段告警数量分布.vue
Normal file
@@ -0,0 +1,514 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="alert-card">
|
||||
<div class="alert-header">告警趋势</div>
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
||||
<el-tab-pane label="今日" name="first">
|
||||
<div ref="todayLineChartDom" class="chart-container"></div> <!-- 今日图表 -->
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="本周" name="second">
|
||||
<div ref="weekLineChartDom" class="chart-container"></div> <!-- 本周图表 -->
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="最近30天" name="third"> <!-- 新增最近30天选项卡 -->
|
||||
<div ref="monthLineChartDom" class="chart-container"></div> <!-- 最近30天图表 -->
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import dayjs from 'dayjs';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const activeName = ref('first');
|
||||
const hourlyCounts = ref(Array(24).fill(0)); // 初始化24小时的计数器
|
||||
const chartInstanceToday = ref(null);
|
||||
const todayLineChartDom = ref(null);
|
||||
|
||||
const dailyCounts = ref(Array(7).fill(0)); // 初始化7天的计数器
|
||||
const weekDates = ref([]); // 存储前7天的日期
|
||||
const chartInstanceWeek = ref(null);
|
||||
const weekLineChartDom = ref(null);
|
||||
|
||||
const monthlyCounts = ref(Array(30).fill(0)); // 初始化30天的计数器
|
||||
const monthDates = ref([]); // 存储前30天的日期
|
||||
const chartInstanceMonth = ref(null); // 30天的图表实例
|
||||
const monthLineChartDom = ref(null);
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const allEvents = ref([]); // 使用响应式 allEvents
|
||||
|
||||
let isTodayChartInitialized = false;
|
||||
let isWeekChartInitialized = false;
|
||||
let isMonthChartInitialized = false; // 30天图表是否初始化
|
||||
|
||||
// 计算前7天的日期
|
||||
const calculateWeekDates = () => {
|
||||
const today = dayjs();
|
||||
weekDates.value = Array.from({ length: 7 }, (_, i) => today.subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
|
||||
// 计算前30天的日期
|
||||
const calculateMonthDates = () => {
|
||||
const today = dayjs();
|
||||
monthDates.value = Array.from({ length: 30 }, (_, i) => today.subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
|
||||
// 初始化今日图表
|
||||
const initTodayChart = () => {
|
||||
if (todayLineChartDom.value) {
|
||||
chartInstanceToday.value = echarts.init(todayLineChartDom.value);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '今日告警数量趋势',
|
||||
top: '3%',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
formatter: function (params) {
|
||||
const { value, name } = params[0];
|
||||
return `${name}<br/>告警数量: ${value}`;
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`),
|
||||
axisLabel: {
|
||||
rotate: 0,
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: hourlyCounts.value.length > 0 ? hourlyCounts.value : Array(24).fill(0),
|
||||
smooth: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'rgb(60,178,239)', // 曲线颜色
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
color: 'rgba(74, 172, 178, 0.3)', // 覆盖面积颜色
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
chartInstanceToday.value.setOption(option);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化本周图表
|
||||
const initWeekChart = () => {
|
||||
if (weekLineChartDom.value) {
|
||||
chartInstanceWeek.value = echarts.init(weekLineChartDom.value);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '最近7天告警数量趋势',
|
||||
top: '5%',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
formatter: function (params) {
|
||||
const { value } = params[0];
|
||||
return `告警数量: ${value}`;
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
data: weekDates.value.length > 0 ? weekDates.value : Array.from({ length: 7 }, (_, i) => `日期 ${i + 1}`),
|
||||
axisLabel: {
|
||||
rotate: 0,
|
||||
interval: 0,
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: dailyCounts.value.length > 0 ? dailyCounts.value : Array(7).fill(0),
|
||||
smooth: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'rgb(60,178,239)', // 曲线颜色
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
color: 'rgba(74, 172, 178, 0.3)', // 覆盖面积颜色
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chartInstanceWeek.value.setOption(option);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化最近30天图表
|
||||
const initMonthChart = () => {
|
||||
if (monthLineChartDom.value) {
|
||||
chartInstanceMonth.value = echarts.init(monthLineChartDom.value);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '最近30天告警数量趋势',
|
||||
top: '3%',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
formatter: function (params) {
|
||||
const { value, name } = params[0];
|
||||
return `${name}<br/>告警数量: ${value}`;
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: monthDates.value,
|
||||
axisLabel: {
|
||||
rotate: 0,
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: monthlyCounts.value.length > 0 ? monthlyCounts.value : Array(30).fill(0),
|
||||
smooth: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'rgb(60,178,239)', // 曲线颜色
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
color: 'rgba(74, 172, 178, 0.3)', // 覆盖面积颜色
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
chartInstanceMonth.value.setOption(option);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取并处理告警数据
|
||||
const fetchAndProcessEvents = async (token) => {
|
||||
try {
|
||||
let currentPage = 1;
|
||||
const pageSize = 1000;
|
||||
|
||||
// 第一次请求,获取告警总数
|
||||
const { tableData: firstBatch, totalItems } = await apiInstance.getEvents(token, pageSize, currentPage);
|
||||
allEvents.value = [...firstBatch]; // 使用 allEvents.value
|
||||
|
||||
// 根据告警总数逐页加载所有数据
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
|
||||
// 循环分页加载剩余的数据
|
||||
while (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
const { tableData: nextBatch } = await apiInstance.getEvents(token, pageSize, currentPage);
|
||||
allEvents.value = [...allEvents.value, ...nextBatch]; // 使用 allEvents.value
|
||||
}
|
||||
|
||||
// 处理数据
|
||||
processEventData(allEvents.value); // 今日告警数据
|
||||
processWeekData(); // 本周告警数据
|
||||
processMonthData(); // 最近30天告警数据
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理今日告警数据并更新图表
|
||||
const processEventData = (events) => {
|
||||
hourlyCounts.value = Array(24).fill(0);
|
||||
const today = new Date();
|
||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
events.forEach((event) => {
|
||||
const endedAt = event.ended_at;
|
||||
if (endedAt) {
|
||||
const endedDate = new Date(endedAt);
|
||||
if (endedDate >= startOfToday && endedDate < endOfToday) {
|
||||
const hour = endedDate.getHours();
|
||||
hourlyCounts.value[hour] += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateCharts();
|
||||
};
|
||||
|
||||
// 处理本周告警数据
|
||||
const processWeekData = () => {
|
||||
dailyCounts.value = Array(7).fill(0);
|
||||
|
||||
allEvents.value.forEach((event) => {
|
||||
const endedAt = event.ended_at;
|
||||
if (endedAt) {
|
||||
const endedDay = dayjs(endedAt).format('YYYY-MM-DD');
|
||||
weekDates.value.forEach((date, index) => {
|
||||
if (endedDay === date) {
|
||||
dailyCounts.value[index] += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
updateWeekChart();
|
||||
};
|
||||
|
||||
// 处理最近30天告警数据
|
||||
const processMonthData = () => {
|
||||
monthlyCounts.value = Array(30).fill(0);
|
||||
|
||||
allEvents.value.forEach((event) => {
|
||||
const endedAt = event.ended_at;
|
||||
if (endedAt) {
|
||||
const endedDay = dayjs(endedAt).format('YYYY-MM-DD');
|
||||
monthDates.value.forEach((date, index) => {
|
||||
if (endedDay === date) {
|
||||
monthlyCounts.value[index] += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
updateMonthChart();
|
||||
};
|
||||
|
||||
// 更新今日图表
|
||||
const updateCharts = () => {
|
||||
if (chartInstanceToday.value) {
|
||||
chartInstanceToday.value.setOption({
|
||||
series: [{ data: hourlyCounts.value }],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 更新本周图表
|
||||
const updateWeekChart = () => {
|
||||
if (chartInstanceWeek.value) {
|
||||
chartInstanceWeek.value.setOption({
|
||||
series: [{ data: dailyCounts.value }],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 更新最近30天图表
|
||||
const updateMonthChart = () => {
|
||||
if (chartInstanceMonth.value) {
|
||||
chartInstanceMonth.value.setOption({
|
||||
series: [{ data: monthlyCounts.value }],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理选项卡切换
|
||||
const handleClick = async (tab) => {
|
||||
if (tab.props.name === 'first') {
|
||||
if (!isTodayChartInitialized) {
|
||||
await nextTick();
|
||||
initTodayChart();
|
||||
isTodayChartInitialized = true;
|
||||
}
|
||||
delayInstanceToday();
|
||||
} else if (tab.props.name === 'second') {
|
||||
await processWeekData();
|
||||
if (!isWeekChartInitialized) {
|
||||
await nextTick();
|
||||
initWeekChart();
|
||||
isWeekChartInitialized = true;
|
||||
}
|
||||
delayInstanceWeek();
|
||||
} else if (tab.props.name === 'third') {
|
||||
await processMonthData();
|
||||
if (!isMonthChartInitialized) {
|
||||
await nextTick();
|
||||
initMonthChart();
|
||||
isMonthChartInitialized = true;
|
||||
}
|
||||
delayInstanceMonth();
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口调整时重新渲染图表
|
||||
const delayInstanceToday = debounce(() => {
|
||||
if (chartInstanceToday.value) {
|
||||
chartInstanceToday.value.resize();
|
||||
updateCharts();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const delayInstanceWeek = debounce(() => {
|
||||
if (chartInstanceWeek.value) {
|
||||
chartInstanceWeek.value.resize();
|
||||
updateWeekChart();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const delayInstanceMonth = debounce(() => {
|
||||
if (chartInstanceMonth.value) {
|
||||
chartInstanceMonth.value.resize();
|
||||
updateMonthChart();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// 组件挂载时调用
|
||||
onMounted(async () => {
|
||||
calculateWeekDates(); // 计算前7天日期
|
||||
calculateMonthDates(); // 计算前30天日期
|
||||
const token = localStorage.getItem('alertToken');
|
||||
await fetchAndProcessEvents(token); // 加载数据
|
||||
|
||||
initTodayChart();
|
||||
});
|
||||
|
||||
// 监听数据变化,更新对应的图表
|
||||
watch(hourlyCounts, () => {
|
||||
if (chartInstanceToday.value) {
|
||||
chartInstanceToday.value.setOption({
|
||||
series: [{ data: hourlyCounts.value }],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(dailyCounts, () => {
|
||||
if (chartInstanceWeek.value) {
|
||||
chartInstanceWeek.value.setOption({
|
||||
series: [{ data: dailyCounts.value }],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(monthlyCounts, () => {
|
||||
if (chartInstanceMonth.value) {
|
||||
chartInstanceMonth.value.setOption({
|
||||
series: [{ data: monthlyCounts.value }],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alert-card {
|
||||
background-color: #304555;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
/* padding: 10px; */
|
||||
/* margin: 10px; */
|
||||
}
|
||||
|
||||
.alert-header {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #3a4b5c;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
/* min-height: 350px; */
|
||||
min-height: 41vh;
|
||||
min-width: 40vw;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: #2ea0ec;
|
||||
}
|
||||
</style>
|
||||
|
||||
411
src/bk/点播页面原版.vue
Normal file
411
src/bk/点播页面原版.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<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;
|
||||
padding-bottom: 10vh;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 每个摄像头项目的样式 */
|
||||
.camera-item {
|
||||
margin-bottom: 10px;
|
||||
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>
|
||||
@@ -1,513 +1,263 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="alert-card">
|
||||
<div class="alert-container">
|
||||
<!-- <el-card class="alert-card">
|
||||
<div class="alert-header">告警趋势</div>
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
||||
<el-tab-pane label="今日" name="first">
|
||||
<div ref="todayLineChartDom" class="chart-container"></div> <!-- 今日图表 -->
|
||||
<div ref="todayLineChartDom" class="chart-container"></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="本周" name="second">
|
||||
<div ref="weekLineChartDom" class="chart-container"></div> <!-- 本周图表 -->
|
||||
<div ref="weekLineChartDom" class="chart-container"></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="最近30天" name="third"> <!-- 新增最近30天选项卡 -->
|
||||
<div ref="monthLineChartDom" class="chart-container"></div> <!-- 最近30天图表 -->
|
||||
<el-tab-pane label="最近30天" name="third">
|
||||
<div ref="monthLineChartDom" class="chart-container"></div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-card> -->
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
||||
<el-tab-pane label="今日" name="first">
|
||||
<div ref="todayLineChartDom" class="chart-container"></div> <!-- 今日图表 -->
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="本周" name="second">
|
||||
<div ref="weekLineChartDom" class="chart-container"></div> <!-- 本周图表 -->
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="最近30天" name="third"> <!-- 新增最近30天选项卡 -->
|
||||
<div ref="monthLineChartDom" class="chart-container"></div> <!-- 最近30天图表 -->
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||
import { ref, onMounted, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import dayjs from 'dayjs';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const activeName = ref('first');
|
||||
const hourlyCounts = ref(Array(24).fill(0)); // 初始化24小时的计数器
|
||||
const chartInstanceToday = ref(null);
|
||||
const hourlyCounts = ref(Array(24).fill(0));
|
||||
const dailyCounts = ref(Array(7).fill(0));
|
||||
const monthlyCounts = ref(Array(30).fill(0));
|
||||
|
||||
const todayLineChartDom = ref(null);
|
||||
|
||||
const dailyCounts = ref(Array(7).fill(0)); // 初始化7天的计数器
|
||||
const weekDates = ref([]); // 存储前7天的日期
|
||||
const chartInstanceWeek = ref(null);
|
||||
const weekLineChartDom = ref(null);
|
||||
|
||||
const monthlyCounts = ref(Array(30).fill(0)); // 初始化30天的计数器
|
||||
const monthDates = ref([]); // 存储前30天的日期
|
||||
const chartInstanceMonth = ref(null); // 30天的图表实例
|
||||
const monthLineChartDom = ref(null);
|
||||
|
||||
const chartInstance = ref(null);
|
||||
|
||||
const weekDates = ref([]);
|
||||
const monthDates = ref([]);
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const allEvents = ref([]); // 使用响应式 allEvents
|
||||
|
||||
let isTodayChartInitialized = false;
|
||||
let isWeekChartInitialized = false;
|
||||
let isMonthChartInitialized = false; // 30天图表是否初始化
|
||||
|
||||
// 计算前7天的日期
|
||||
const calculateWeekDates = () => {
|
||||
const today = dayjs();
|
||||
weekDates.value = Array.from({ length: 7 }, (_, i) => today.subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
|
||||
// 计算前30天的日期
|
||||
const calculateMonthDates = () => {
|
||||
const today = dayjs();
|
||||
monthDates.value = Array.from({ length: 30 }, (_, i) => today.subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
|
||||
// 初始化今日图表
|
||||
const initTodayChart = () => {
|
||||
if (todayLineChartDom.value) {
|
||||
chartInstanceToday.value = echarts.init(todayLineChartDom.value);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '今日告警数量趋势',
|
||||
top: '3%',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
formatter: function (params) {
|
||||
const { value, name } = params[0];
|
||||
return `${name}<br/>告警数量: ${value}`;
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`),
|
||||
axisLabel: {
|
||||
rotate: 0,
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: hourlyCounts.value.length > 0 ? hourlyCounts.value : Array(24).fill(0),
|
||||
smooth: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'rgb(60,178,239)', // 曲线颜色
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
color: 'rgba(74, 172, 178, 0.3)', // 覆盖面积颜色
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
chartInstanceToday.value.setOption(option);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化本周图表
|
||||
const initWeekChart = () => {
|
||||
if (weekLineChartDom.value) {
|
||||
chartInstanceWeek.value = echarts.init(weekLineChartDom.value);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '最近7天告警数量趋势',
|
||||
top: '5%',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
formatter: function (params) {
|
||||
const { value } = params[0];
|
||||
return `告警数量: ${value}`;
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
data: weekDates.value.length > 0 ? weekDates.value : Array.from({ length: 7 }, (_, i) => `日期 ${i + 1}`),
|
||||
axisLabel: {
|
||||
rotate: 0,
|
||||
interval: 0,
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: dailyCounts.value.length > 0 ? dailyCounts.value : Array(7).fill(0),
|
||||
smooth: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'rgb(60,178,239)', // 曲线颜色
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
color: 'rgba(74, 172, 178, 0.3)', // 覆盖面积颜色
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chartInstanceWeek.value.setOption(option);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化最近30天图表
|
||||
const initMonthChart = () => {
|
||||
if (monthLineChartDom.value) {
|
||||
chartInstanceMonth.value = echarts.init(monthLineChartDom.value);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '最近30天告警数量趋势',
|
||||
top: '3%',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
formatter: function (params) {
|
||||
const { value, name } = params[0];
|
||||
return `${name}<br/>告警数量: ${value}`;
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: monthDates.value,
|
||||
axisLabel: {
|
||||
rotate: 0,
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: monthlyCounts.value.length > 0 ? monthlyCounts.value : Array(30).fill(0),
|
||||
smooth: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
lineStyle: {
|
||||
color: 'rgb(60,178,239)', // 曲线颜色
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
color: 'rgba(74, 172, 178, 0.3)', // 覆盖面积颜色
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
chartInstanceMonth.value.setOption(option);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取并处理告警数据
|
||||
const fetchAndProcessEvents = async (token) => {
|
||||
// 设置时间范围获取计数
|
||||
const getEventCount = async (timeBefore, timeAfter) => {
|
||||
try {
|
||||
let currentPage = 1;
|
||||
const pageSize = 1000;
|
||||
|
||||
// 第一次请求,获取告警总数
|
||||
const { tableData: firstBatch, totalItems } = await apiInstance.getEvents(token, pageSize, currentPage);
|
||||
allEvents.value = [...firstBatch]; // 使用 allEvents.value
|
||||
|
||||
// 根据告警总数逐页加载所有数据
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
|
||||
// 循环分页加载剩余的数据
|
||||
while (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
const { tableData: nextBatch } = await apiInstance.getEvents(token, pageSize, currentPage);
|
||||
allEvents.value = [...allEvents.value, ...nextBatch]; // 使用 allEvents.value
|
||||
}
|
||||
|
||||
// 处理数据
|
||||
processEventData(allEvents.value); // 今日告警数据
|
||||
processWeekData(); // 本周告警数据
|
||||
processMonthData(); // 最近30天告警数据
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const response = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter);
|
||||
return response.count;
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
console.error("Error fetching event count:", error);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理今日告警数据并更新图表
|
||||
const processEventData = (events) => {
|
||||
hourlyCounts.value = Array(24).fill(0);
|
||||
const today = new Date();
|
||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
// 生成图表的配置选项
|
||||
const createChartOption = (title, xAxisData, seriesData) => ({
|
||||
title: {
|
||||
text: title,
|
||||
left: 'center',
|
||||
top: '0%',
|
||||
textStyle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`;
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic',
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: { color: '#cccccc', width: 1, type: 'solid' }
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: seriesData,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
events.forEach((event) => {
|
||||
const endedAt = event.ended_at;
|
||||
if (endedAt) {
|
||||
const endedDate = new Date(endedAt);
|
||||
if (endedDate >= startOfToday && endedDate < endOfToday) {
|
||||
const hour = endedDate.getHours();
|
||||
hourlyCounts.value[hour] += 1;
|
||||
}
|
||||
// 更新计数数据
|
||||
const updateCounts = async (range) => {
|
||||
let timeAfter, timeBefore = dayjs().format();
|
||||
|
||||
if (range === 'day') {
|
||||
// 获取“今日”每小时的事件数量
|
||||
for (let i = 0; i < 24; i++) {
|
||||
timeAfter = dayjs().startOf('day').add(i, 'hour').format();
|
||||
timeBefore = dayjs().startOf('day').add(i + 1, 'hour').format();
|
||||
hourlyCounts.value[i] = await getEventCount(timeBefore, timeAfter);
|
||||
console.log(`Hour ${i}: ${hourlyCounts.value[i]}`);
|
||||
}
|
||||
});
|
||||
delayedInitChart(todayLineChartDom, createChartOption('今日告警趋势', Array.from({ length: 24 }, (_, i) => `${i}:00`), hourlyCounts.value));
|
||||
|
||||
updateCharts();
|
||||
};
|
||||
|
||||
// 处理本周告警数据
|
||||
const processWeekData = () => {
|
||||
dailyCounts.value = Array(7).fill(0);
|
||||
|
||||
allEvents.value.forEach((event) => {
|
||||
const endedAt = event.ended_at;
|
||||
if (endedAt) {
|
||||
const endedDay = dayjs(endedAt).format('YYYY-MM-DD');
|
||||
weekDates.value.forEach((date, index) => {
|
||||
if (endedDay === date) {
|
||||
dailyCounts.value[index] += 1;
|
||||
}
|
||||
});
|
||||
} else if (range === 'week') {
|
||||
// 获取“本周”每天的事件数量
|
||||
for (let i = 0; i < 7; i++) {
|
||||
timeAfter = dayjs().subtract(i + 1, 'day').endOf('day').format();
|
||||
timeBefore = dayjs().subtract(i, 'day').endOf('day').format();
|
||||
dailyCounts.value[6 - i] = await getEventCount(timeBefore, timeAfter);
|
||||
}
|
||||
});
|
||||
delayedInitChart(weekLineChartDom, createChartOption('本周告警趋势', weekDates.value, dailyCounts.value));
|
||||
|
||||
updateWeekChart();
|
||||
};
|
||||
|
||||
// 处理最近30天告警数据
|
||||
const processMonthData = () => {
|
||||
monthlyCounts.value = Array(30).fill(0);
|
||||
|
||||
allEvents.value.forEach((event) => {
|
||||
const endedAt = event.ended_at;
|
||||
if (endedAt) {
|
||||
const endedDay = dayjs(endedAt).format('YYYY-MM-DD');
|
||||
monthDates.value.forEach((date, index) => {
|
||||
if (endedDay === date) {
|
||||
monthlyCounts.value[index] += 1;
|
||||
}
|
||||
});
|
||||
} else if (range === 'month') {
|
||||
// 获取“最近30天”每天的事件数量
|
||||
for (let i = 0; i < 30; i++) {
|
||||
timeAfter = dayjs().subtract(i + 1, 'day').endOf('day').format();
|
||||
timeBefore = dayjs().subtract(i, 'day').endOf('day').format();
|
||||
monthlyCounts.value[29 - i] = await getEventCount(timeBefore, timeAfter);
|
||||
}
|
||||
});
|
||||
|
||||
updateMonthChart();
|
||||
};
|
||||
|
||||
// 更新今日图表
|
||||
const updateCharts = () => {
|
||||
if (chartInstanceToday.value) {
|
||||
chartInstanceToday.value.setOption({
|
||||
series: [{ data: hourlyCounts.value }],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 更新本周图表
|
||||
const updateWeekChart = () => {
|
||||
if (chartInstanceWeek.value) {
|
||||
chartInstanceWeek.value.setOption({
|
||||
series: [{ data: dailyCounts.value }],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 更新最近30天图表
|
||||
const updateMonthChart = () => {
|
||||
if (chartInstanceMonth.value) {
|
||||
chartInstanceMonth.value.setOption({
|
||||
series: [{ data: monthlyCounts.value }],
|
||||
});
|
||||
delayedInitChart(monthLineChartDom, createChartOption('近30天告警趋势', monthDates.value, monthlyCounts.value));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理选项卡切换
|
||||
const handleClick = async (tab) => {
|
||||
if (tab.props.name === 'first') {
|
||||
if (!isTodayChartInitialized) {
|
||||
await nextTick();
|
||||
initTodayChart();
|
||||
isTodayChartInitialized = true;
|
||||
}
|
||||
delayInstanceToday();
|
||||
} else if (tab.props.name === 'second') {
|
||||
await processWeekData();
|
||||
if (!isWeekChartInitialized) {
|
||||
await nextTick();
|
||||
initWeekChart();
|
||||
isWeekChartInitialized = true;
|
||||
}
|
||||
delayInstanceWeek();
|
||||
} else if (tab.props.name === 'third') {
|
||||
await processMonthData();
|
||||
if (!isMonthChartInitialized) {
|
||||
await nextTick();
|
||||
initMonthChart();
|
||||
isMonthChartInitialized = true;
|
||||
}
|
||||
delayInstanceMonth();
|
||||
}
|
||||
const range = tab.props.name === 'first' ? 'day' : tab.props.name === 'second' ? 'week' : 'month';
|
||||
await updateCounts(range);
|
||||
};
|
||||
|
||||
// 窗口调整时重新渲染图表
|
||||
const delayInstanceToday = debounce(() => {
|
||||
if (chartInstanceToday.value) {
|
||||
chartInstanceToday.value.resize();
|
||||
updateCharts();
|
||||
}
|
||||
}, 300);
|
||||
// 初始化日期
|
||||
const calculateWeekDates = () => {
|
||||
weekDates.value = Array.from({ length: 7 }, (_, i) => dayjs().subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
const calculateMonthDates = () => {
|
||||
monthDates.value = Array.from({ length: 30 }, (_, i) => dayjs().subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
|
||||
const delayInstanceWeek = debounce(() => {
|
||||
if (chartInstanceWeek.value) {
|
||||
chartInstanceWeek.value.resize();
|
||||
updateWeekChart();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const delayInstanceMonth = debounce(() => {
|
||||
if (chartInstanceMonth.value) {
|
||||
chartInstanceMonth.value.resize();
|
||||
updateMonthChart();
|
||||
}
|
||||
// 图表延迟初始化
|
||||
const delayedInitChart = debounce((domRef, option) => {
|
||||
nextTick(() => {
|
||||
if (domRef.value) {
|
||||
chartInstance.value = echarts.init(domRef.value);
|
||||
chartInstance.value.setOption(option);
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
|
||||
// 组件挂载时调用
|
||||
onMounted(async () => {
|
||||
calculateWeekDates(); // 计算前7天日期
|
||||
calculateMonthDates(); // 计算前30天日期
|
||||
const token = localStorage.getItem('alertToken');
|
||||
await fetchAndProcessEvents(token); // 加载数据
|
||||
|
||||
initTodayChart();
|
||||
calculateWeekDates();
|
||||
calculateMonthDates();
|
||||
await handleClick({ props: { name: 'first' } });
|
||||
window.addEventListener('resize', debounce(() => {
|
||||
if (chartInstance.value) chartInstance.value.resize();
|
||||
}, 300));
|
||||
});
|
||||
|
||||
// 监听数据变化,更新对应的图表
|
||||
watch(hourlyCounts, () => {
|
||||
if (chartInstanceToday.value) {
|
||||
chartInstanceToday.value.setOption({
|
||||
series: [{ data: hourlyCounts.value }],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(dailyCounts, () => {
|
||||
if (chartInstanceWeek.value) {
|
||||
chartInstanceWeek.value.setOption({
|
||||
series: [{ data: dailyCounts.value }],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(monthlyCounts, () => {
|
||||
if (chartInstanceMonth.value) {
|
||||
chartInstanceMonth.value.setOption({
|
||||
series: [{ data: monthlyCounts.value }],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
.alert-card {
|
||||
background-color: #304555;
|
||||
background-color: #001529;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
/* padding: 10px; */
|
||||
/* margin: 10px; */
|
||||
}
|
||||
|
||||
.alert-header {
|
||||
.alert-container {
|
||||
background-color: #001529;
|
||||
/* margin: 1vh 0vw 0vh 1vw; */
|
||||
/* margin: 0; */
|
||||
/* margin-top: 0; */
|
||||
padding: 2vh;
|
||||
}
|
||||
|
||||
/* .alert-header {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #3a4b5c;
|
||||
}
|
||||
} */
|
||||
|
||||
.chart-container {
|
||||
/* min-height: 350px; */
|
||||
min-height: 41vh;
|
||||
min-width: 40vw;
|
||||
width: 100%;
|
||||
height: 21vh;
|
||||
margin: 1vh 0;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
padding: 0;
|
||||
margin-left: 1vh;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: #2ea0ec;
|
||||
}
|
||||
|
||||
/* ::v-deep .el-tabs___nav-wrap.is-top::after{
|
||||
border: none;
|
||||
} */
|
||||
|
||||
/* ::v-deep .el-tabs__active-bar.is-top{
|
||||
padding: 0 204px;
|
||||
box-sizing: border-box !important;
|
||||
background-clip: content-box !important;
|
||||
} */
|
||||
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
/* width: 15vw; */
|
||||
position: static !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
350
src/components/Channel.vue
Normal file
350
src/components/Channel.vue
Normal file
@@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<div class="camera-container">
|
||||
<div class="top-header">
|
||||
<el-select v-model="filterStatus" placeholder="筛选状态" @change="fetchCameras" class="status-filter" >
|
||||
<el-option label="全部" value="all"></el-option>
|
||||
<el-option label="在线" value="online"></el-option>
|
||||
<el-option label="离线" value="offline"></el-option>
|
||||
</el-select>
|
||||
<div class="top-text">警戒点位</div>
|
||||
<el-select v-model="selectedCameraId" placeholder="搜索摄像头名称" @change="selectCameraById" clearable filterable
|
||||
class="camera-select" >
|
||||
<el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
|
||||
<span>{{ camera.id }}.</span> 名称: {{ camera.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 播放列表,设置为可滚动 -->
|
||||
<div class="camera-grid">
|
||||
<div v-for="(camera, index) in selectedCameras" :key="camera.id" class="grid-item">
|
||||
<div class="stream-control">
|
||||
<p class="camera-name-title">{{ camera.name }}</p>
|
||||
<div class="close-button" @click="closeStream(camera)">×</div>
|
||||
</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" />
|
||||
<el-button v-show="!camera.playing || showButton" class="play-button" type="primary" circle size="large"
|
||||
@click="openDialog(camera)">
|
||||
<el-icon>
|
||||
<VideoPlay v-if="!camera.playing" />
|
||||
<VideoPause v-if="camera.playing" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗播放视频 -->
|
||||
<el-dialog v-model="dialogVisible" width="50%" @close="closeDialog">
|
||||
<template #title>播放摄像头: {{ currentCamera?.name }}</template>
|
||||
<canvas v-show="dialogVisible" ref="dialogCanvas" class="dialog-canvas"></canvas>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } 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);
|
||||
const apiInstance = new BoxApi();
|
||||
const canvasRefs = ref({});
|
||||
const selectedCameraId = ref(null);
|
||||
const dialogVisible = ref(false); // 控制弹窗的显示与隐藏
|
||||
const currentCamera = ref(null); // 当前选中的摄像头
|
||||
|
||||
// 弹窗中的canvas引用
|
||||
const dialogCanvas = ref(null);
|
||||
const filterStatus = ref("all");
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const cameraData = await apiInstance.getMinCameras(token);
|
||||
|
||||
// 根据 filterStatus 筛选摄像头状态
|
||||
if (filterStatus.value === "online") {
|
||||
cameras.value = cameraData.filter(camera => camera.status === "online");
|
||||
} else if (filterStatus.value === "offline") {
|
||||
cameras.value = cameraData.filter(camera => camera.status === "offline");
|
||||
} else {
|
||||
cameras.value = cameraData; // 默认 "all" 显示全部摄像头
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取摄像头列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const selectCameraById = (cameraId) => {
|
||||
const camera = cameras.value.find(c => c.id === cameraId);
|
||||
if (camera && !selectedCameras.value.some(c => c.id === camera.id)) {
|
||||
selectedCameras.value.push({ ...camera, playing: false, streamPort: null });
|
||||
}
|
||||
};
|
||||
|
||||
// 打开弹窗并开始播放
|
||||
const openDialog = async (camera) => {
|
||||
currentCamera.value = camera;
|
||||
dialogVisible.value = true;
|
||||
await nextTick();
|
||||
startStreamInDialog(camera);
|
||||
};
|
||||
|
||||
// 在弹窗中播放视频
|
||||
const startStreamInDialog = async (camera) => {
|
||||
const canvas = dialogCanvas.value;
|
||||
|
||||
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 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 closeDialog = () => {
|
||||
if (currentCamera.value) {
|
||||
handleStopStream(currentCamera.value);
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
currentCamera.value = null;
|
||||
};
|
||||
|
||||
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;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
position: relative;
|
||||
top: 0vh;
|
||||
left: 0vw;
|
||||
width: 5vw;
|
||||
margin-left: 1vh;
|
||||
margin-top: 1vh;
|
||||
|
||||
}
|
||||
::v-deep .status-filter .el-select__wrapper{
|
||||
background-color: #001529;
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::v-deep .camera-select .el-select__wrapper{
|
||||
background-color: #001529;
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::v-deep .camera-select .el-select__selected-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
::v-deep .status-filter .el-select__selected-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.camera-select {
|
||||
position: relative;
|
||||
top: 0vh;
|
||||
left: 0vw;
|
||||
width: 12vw;
|
||||
margin-left: 0vh;
|
||||
margin-top: 1vh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.top-text {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
width: 7vw;
|
||||
margin: 1vh 0 0 0;
|
||||
padding: 0 0 0 10vw;
|
||||
/* justify-content: center; */
|
||||
align-content: center;
|
||||
background-color: #001529;
|
||||
line-height: 0px;
|
||||
color: aliceblue;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stream-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.camera-name-title {
|
||||
padding: 0.2vh;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.camera-grid {
|
||||
margin: 0vh 1vh;
|
||||
padding: 0 0 2vh 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1vh 0vh;
|
||||
/* height: 39vh;
|
||||
width: 34vw; */
|
||||
width: 68vw;
|
||||
height: 55vh;
|
||||
max-height: 58vh;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
/* background-color: #001529; */
|
||||
background-color: #001529;
|
||||
}
|
||||
|
||||
.camera-grid::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
margin: 1vh;
|
||||
position: relative;
|
||||
height: 25vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.camera-snapshot,
|
||||
.camera-large {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: #000000;
|
||||
color: aliceblue;
|
||||
padding: 0 2px 2px 0;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.play-button-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 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;
|
||||
}
|
||||
|
||||
.dialog-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
|
||||
const activeName = ref('first');
|
||||
const hourlyCounts = ref(Array(24).fill(0));
|
||||
const dailyCounts = ref(Array(7).fill(0));
|
||||
@@ -44,38 +45,128 @@
|
||||
const weekLineChartDom = ref(null);
|
||||
const monthLineChartDom = ref(null);
|
||||
|
||||
const chartInstanceToday = ref(null);
|
||||
const chartInstanceWeek = ref(null);
|
||||
const chartInstanceMonth = ref(null);
|
||||
const chartInstance = ref(null);
|
||||
|
||||
const weekDates = ref([]);
|
||||
const monthDates = ref([]);
|
||||
|
||||
let isTodayChartInitialized = false;
|
||||
let isWeekChartInitialized = false;
|
||||
let isMonthChartInitialized = false;
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const allEvents = ref([]);
|
||||
|
||||
// 日期格式化函数
|
||||
const formatDateTime = (date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
// 获取时间参数
|
||||
const getDayParams = () => {
|
||||
const today = dayjs();
|
||||
return { timeAfter: today.startOf('day').format(), timeBefore: today.endOf('day').format() };
|
||||
};
|
||||
const getWeeklyParams = () => {
|
||||
const today = dayjs();
|
||||
return { timeAfter: today.subtract(7, 'day').format(), timeBefore: today.endOf('day').format() };
|
||||
};
|
||||
const getMonthlyParams = () => {
|
||||
const today = dayjs();
|
||||
return { timeAfter: today.subtract(30, 'day').format(), timeBefore: today.endOf('day').format() };
|
||||
// 设置时间范围获取计数
|
||||
const getEventCount = async (timeBefore, timeAfter) => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const response = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter);
|
||||
return response.count;
|
||||
} catch (error) {
|
||||
console.error("Error fetching event count:", error);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// 计算日期
|
||||
// 生成图表的配置选项
|
||||
const createChartOption = (title, xAxisData, seriesData) => ({
|
||||
// title: {
|
||||
// text: title,
|
||||
// left: 'center',
|
||||
// top: '0%',
|
||||
// textStyle: {
|
||||
// color: '#ffffff',
|
||||
// fontSize: 16,
|
||||
// fontWeight: 'bold',
|
||||
// fontFamily: 'Arial, sans-serif',
|
||||
// },
|
||||
// },
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`;
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic',
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: { color: '#cccccc', width: 1, type: 'solid' }
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: seriesData,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 更新计数数据
|
||||
const updateCounts = async (range) => {
|
||||
let timeAfter, timeBefore = dayjs().format();
|
||||
|
||||
if (range === 'day') {
|
||||
// 获取“今日”每小时的事件数量
|
||||
for (let i = 0; i < 24; i++) {
|
||||
timeAfter = dayjs().startOf('day').add(i, 'hour').format();
|
||||
timeBefore = dayjs().startOf('day').add(i + 1, 'hour').format();
|
||||
hourlyCounts.value[i] = await getEventCount(timeBefore, timeAfter);
|
||||
console.log(`Hour ${i}: ${hourlyCounts.value[i]}`);
|
||||
}
|
||||
delayedInitChart(todayLineChartDom, createChartOption('今日告警趋势', Array.from({ length: 24 }, (_, i) => `${i}:00`), hourlyCounts.value));
|
||||
|
||||
} else if (range === 'week') {
|
||||
// 获取“本周”每天的事件数量
|
||||
for (let i = 0; i < 7; i++) {
|
||||
timeAfter = dayjs().subtract(i + 1, 'day').endOf('day').format();
|
||||
timeBefore = dayjs().subtract(i, 'day').endOf('day').format();
|
||||
dailyCounts.value[6 - i] = await getEventCount(timeBefore, timeAfter);
|
||||
}
|
||||
delayedInitChart(weekLineChartDom, createChartOption('本周告警趋势', weekDates.value, dailyCounts.value));
|
||||
|
||||
} else if (range === 'month') {
|
||||
// 获取“最近30天”每天的事件数量
|
||||
for (let i = 0; i < 30; i++) {
|
||||
timeAfter = dayjs().subtract(i + 1, 'day').endOf('day').format();
|
||||
timeBefore = dayjs().subtract(i, 'day').endOf('day').format();
|
||||
monthlyCounts.value[29 - i] = await getEventCount(timeBefore, timeAfter);
|
||||
}
|
||||
delayedInitChart(monthLineChartDom, createChartOption('近30天告警趋势', monthDates.value, monthlyCounts.value));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理选项卡切换
|
||||
const handleClick = async (tab) => {
|
||||
const range = tab.props.name === 'first' ? 'day' : tab.props.name === 'second' ? 'week' : 'month';
|
||||
await updateCounts(range);
|
||||
};
|
||||
|
||||
// 初始化日期
|
||||
const calculateWeekDates = () => {
|
||||
weekDates.value = Array.from({ length: 7 }, (_, i) => dayjs().subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
@@ -83,8 +174,8 @@
|
||||
monthDates.value = Array.from({ length: 30 }, (_, i) => dayjs().subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
|
||||
// 延迟初始化图表函数
|
||||
const delayedInitChart = debounce((chartInstance, domRef, option) => {
|
||||
// 图表延迟初始化
|
||||
const delayedInitChart = debounce((domRef, option) => {
|
||||
nextTick(() => {
|
||||
if (domRef.value) {
|
||||
chartInstance.value = echarts.init(domRef.value);
|
||||
@@ -93,253 +184,14 @@
|
||||
});
|
||||
}, 300);
|
||||
|
||||
// 初始化图表选项配置
|
||||
const createTodayChartOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
console.log('Tooltip params:', params);
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`; // 使用 <strong> 标签作为小标题
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic', // 字体样式,可以是 'normal', 'italic', 'oblique'
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif', // 字体
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: Array.from({ length: 24 }, (_, i) => `${i}:00`),
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: hourlyCounts.value,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createWeekChartOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
console.log('Tooltip params:', params);
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`; // 使用 <strong> 标签作为小标题
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic', // 字体样式,可以是 'normal', 'italic', 'oblique'
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif', // 字体
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: weekDates.value,
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: dailyCounts.value,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createMonthChartOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
console.log('Tooltip params:', params);
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`; // 使用 <strong> 标签作为小标题
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic', // 字体样式,可以是 'normal', 'italic', 'oblique'
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif', // 字体
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: monthDates.value,
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: monthlyCounts.value,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 获取和处理事件数据
|
||||
const fetchAndProcessEvents = async (timeParams) => {
|
||||
try {
|
||||
let currentPage = 1;
|
||||
const pageSize = 1000;
|
||||
const token = localStorage.getItem('alertToken');
|
||||
allEvents.value = [];
|
||||
|
||||
const firstResponse = await apiInstance.getEventsByParams(token, pageSize, currentPage, timeParams.timeBefore, timeParams.timeAfter);
|
||||
const totalItems = firstResponse.count;
|
||||
allEvents.value.push(...firstResponse.results);
|
||||
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
while (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
const response = await apiInstance.getEventsByParams(token, pageSize, currentPage, timeParams.timeBefore, timeParams.timeAfter);
|
||||
allEvents.value.push(...response.results);
|
||||
}
|
||||
|
||||
processEventData(allEvents.value);
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理事件数据
|
||||
const processEventData = (events) => {
|
||||
hourlyCounts.value.fill(0);
|
||||
dailyCounts.value.fill(0);
|
||||
monthlyCounts.value.fill(0);
|
||||
events.forEach((event) => {
|
||||
const hour = dayjs(event.ended_at).hour();
|
||||
const dayDiff = dayjs().diff(dayjs(event.ended_at), 'day');
|
||||
if (dayDiff < 1) hourlyCounts.value[hour] += 1;
|
||||
if (dayDiff < 7) dailyCounts.value[6 - dayDiff] += 1;
|
||||
if (dayDiff < 30) monthlyCounts.value[29 - dayDiff] += 1;
|
||||
});
|
||||
updateCharts();
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateCharts = () => {
|
||||
if (chartInstanceToday.value) chartInstanceToday.value.setOption({ series: [{ data: hourlyCounts.value }] });
|
||||
if (chartInstanceWeek.value) chartInstanceWeek.value.setOption({ series: [{ data: dailyCounts.value }] });
|
||||
if (chartInstanceMonth.value) chartInstanceMonth.value.setOption({ series: [{ data: monthlyCounts.value }] });
|
||||
};
|
||||
|
||||
// 处理选项卡切换
|
||||
const handleClick = async (tab) => {
|
||||
let timeParams;
|
||||
if (tab.props.name === 'first' && !isTodayChartInitialized) {
|
||||
timeParams = getDayParams();
|
||||
await fetchAndProcessEvents(timeParams);
|
||||
delayedInitChart(chartInstanceToday, todayLineChartDom, createTodayChartOption());
|
||||
isTodayChartInitialized = true;
|
||||
} else if (tab.props.name === 'second' && !isWeekChartInitialized) {
|
||||
timeParams = getWeeklyParams();
|
||||
await fetchAndProcessEvents(timeParams);
|
||||
delayedInitChart(chartInstanceWeek, weekLineChartDom, createWeekChartOption());
|
||||
isWeekChartInitialized = true;
|
||||
} else if (tab.props.name === 'third' && !isMonthChartInitialized) {
|
||||
timeParams = getMonthlyParams();
|
||||
await fetchAndProcessEvents(timeParams);
|
||||
delayedInitChart(chartInstanceMonth, monthLineChartDom, createMonthChartOption());
|
||||
isMonthChartInitialized = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口调整时重新渲染图表
|
||||
const resizeCharts = debounce(() => {
|
||||
if (chartInstanceToday.value) chartInstanceToday.value.resize();
|
||||
if (chartInstanceWeek.value) chartInstanceWeek.value.resize();
|
||||
if (chartInstanceMonth.value) chartInstanceMonth.value.resize();
|
||||
}, 300);
|
||||
|
||||
// 组件挂载时调用
|
||||
onMounted(async () => {
|
||||
calculateWeekDates();
|
||||
calculateMonthDates();
|
||||
await handleClick({ props: { name: 'first' } });
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
window.addEventListener('resize', debounce(() => {
|
||||
if (chartInstance.value) chartInstance.value.resize();
|
||||
}, 300));
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -146,6 +146,11 @@ const originalWidths = [97, 150, 160]; // 默认宽度
|
||||
const adjustedWidths = ref([...originalWidths]);
|
||||
const baseWidth = 2150;
|
||||
|
||||
const formatDateTimeToISO = (datetime) => {
|
||||
return new Date(datetime).toISOString().replace('.000', '');
|
||||
};
|
||||
|
||||
|
||||
const adjustColumnWidths = () => {
|
||||
const currentWidth = window.innerWidth;
|
||||
// console.log(">>>>>>>>>>", currentWidth);
|
||||
@@ -192,8 +197,8 @@ const fetchEvents = async () => {
|
||||
|
||||
const { endOfToday, startOfToday } = getDateParams();
|
||||
|
||||
const timeBefore = formatDateTime(endOfToday);
|
||||
const timeAfter = formatDateTime(startOfToday);
|
||||
const timeBefore = formatDateTimeToISO(endOfToday);
|
||||
const timeAfter = formatDateTimeToISO(startOfToday);
|
||||
// console.log("Start of today:", timeAfter);
|
||||
// const firstResponse = await apiInstance.getEventsByParams(token, limit, currentPage);
|
||||
const firstResponse = await apiInstance.getEventsByParams(token, limit, currentPage,timeBefore, timeAfter);
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ pendintingEventCount }}
|
||||
{{ pendingEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
@@ -169,7 +169,7 @@
|
||||
const cameraOfflineCount = ref(0);
|
||||
const cameraOnlineCount = ref(0);
|
||||
const eventCount = ref(0);
|
||||
const pendintingEventCount = ref(0);
|
||||
const pendingEventCount = ref(0);
|
||||
const closedEventCount = ref(0);
|
||||
|
||||
const activeTab = ref('all');
|
||||
@@ -227,7 +227,7 @@
|
||||
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
cameraCount.value = firstResponse.count;
|
||||
console.log("总数》》》》》》》》》》》》》", cameraCount.value)
|
||||
// console.log("总数》》》》》》》》》》》》》", cameraCount.value)
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
// eventCount.value = totalResponse.count;
|
||||
|
||||
// const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
|
||||
// pendintingEventCount.value = pendingResponse.count;
|
||||
// pendingEventCount.value = pendingResponse.count;
|
||||
|
||||
// const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'closed');
|
||||
// closedEventCount.value = closedResponse.count;
|
||||
@@ -292,7 +292,7 @@
|
||||
|
||||
// 获取状态为 pending 的告警数量
|
||||
const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'pending');
|
||||
pendintingEventCount.value = pendingResponse.count;
|
||||
pendingEventCount.value = pendingResponse.count;
|
||||
|
||||
// 获取状态为 closed 的告警数量
|
||||
const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'closed');
|
||||
|
||||
@@ -38,6 +38,10 @@ import * as echarts from 'echarts';
|
||||
const apiInstance = new BoxApi();
|
||||
const typeMapping = reactive({});
|
||||
const typeCounts = reactive({}); // 存储每种类型的数量
|
||||
const formatDateTimeToISO = (datetime) => {
|
||||
return new Date(datetime).toISOString().replace('.000', '');
|
||||
};
|
||||
|
||||
|
||||
const filterParams = reactive({
|
||||
timeAfter: null,
|
||||
@@ -195,8 +199,8 @@ const fetchTypeCounts = async (timeAfter = null, timeBefore = null) => {
|
||||
|
||||
// 点击查询按钮时,添加时间条件并重新统计
|
||||
const handleFilter = () => {
|
||||
const timeAfter = filterParams.timeAfter ? formatDateTime(new Date(filterParams.timeAfter)) : null;
|
||||
const timeBefore = filterParams.timeBefore ? formatDateTime(new Date(filterParams.timeBefore)) : null;
|
||||
const timeAfter = filterParams.timeAfter ? formatDateTimeToISO(new Date(filterParams.timeAfter)) : null;
|
||||
const timeBefore = filterParams.timeBefore ? formatDateTimeToISO(new Date(filterParams.timeBefore)) : null;
|
||||
fetchTypeCounts(timeAfter, timeBefore); // 重新统计数量,添加时间条件
|
||||
|
||||
};
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<template>
|
||||
<div class="settings-container">
|
||||
|
||||
<el-row class="popup-row">
|
||||
<el-checkbox v-model="isPopupEnabled" @change="handleCheckboxChange">
|
||||
<el-checkbox v-model="isPopupEnabled" @change="handleCheckboxChange" style="color: aliceblue;">
|
||||
开启弹窗
|
||||
</el-checkbox>
|
||||
</el-row>
|
||||
|
||||
<el-row class="channel-row">
|
||||
<Channel/>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, inject, onMounted } from 'vue';
|
||||
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||
import type { GlobalWebSocket } from '../utils/useGlobalWebSocket'; // 引入类型
|
||||
import type { GlobalWebSocket } from '../utils/useGlobalWebSocket';
|
||||
import Channel from '@/components/Channel.vue';
|
||||
|
||||
const isPopupEnabled = ref(false); // 控制弹窗状态
|
||||
const globalWebSocket = inject<GlobalWebSocket>('globalWebSocket'); // 指定类型
|
||||
@@ -61,9 +67,30 @@ const handleCheckboxChange = () => {
|
||||
|
||||
<style scoped>
|
||||
.settings-container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #ffffff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
.popup-row {
|
||||
margin-bottom: 20px;
|
||||
height: 20vh;
|
||||
width: 80vw;
|
||||
padding: 1vh 1vw;
|
||||
margin: 1vh 2vw;
|
||||
background-color: #001529;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.channel-row{
|
||||
margin: 1vh 2vw;
|
||||
width: 80vw;
|
||||
height: 70vh;
|
||||
background-color: #001529;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -8,40 +8,52 @@
|
||||
<alertChart />
|
||||
</el-col>
|
||||
</el-row> -->
|
||||
<el-row class="middle-row">
|
||||
<!-- <el-row class="middle-row">
|
||||
<span>
|
||||
告警总数:{{ displayTotalItems }}
|
||||
</span>
|
||||
</el-row>
|
||||
</el-row> -->
|
||||
<el-row class="filter-row">
|
||||
<el-col :span="5">
|
||||
<el-col :span="4">
|
||||
<el-form-item label="摄像头名称">
|
||||
<el-select v-model="filterParams.cameraId" placeholder="请选择摄像头" filterable>
|
||||
<el-select v-model="filterParams.cameraId" placeholder="请选择摄像头" filterable clearable>
|
||||
<el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-form-item label="告警类型">
|
||||
<el-select v-model="filterParams.types" placeholder="请选择告警类型">
|
||||
<el-select v-model="filterParams.types" placeholder="请选择类型" clearable>
|
||||
<el-option v-for="(label, key) in typeMapping" :key="key" :label="label" :value="key"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<el-col :span="3">
|
||||
<el-form-item label="处理情况">
|
||||
<el-select v-model="filterParams.status" placeholder="全部" clearable>
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="待处理" value="pending"></el-option>
|
||||
<el-option label="已处理" value="closed"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="开始时间">
|
||||
<el-date-picker v-model="filterParams.timeAfter" type="datetime" placeholder="请选择开始时间"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<el-col :span="4">
|
||||
<el-form-item label="结束时间">
|
||||
<el-date-picker v-model="filterParams.timeBefore" type="datetime" placeholder="请选择结束时间"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="2" class="filter-buttons">
|
||||
<el-col :span="4" class="filter-buttons">
|
||||
<el-button type="primary" @click="handleFilter">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button :disabled="isExporting" @click="exportData">
|
||||
{{ isExporting ? '正在导出,请勿重复点击' : '导出' }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
@@ -181,6 +193,9 @@ import { ref, reactive, onMounted, computed } from 'vue';
|
||||
// import AlertChart from '@/components/AlertChart.vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
// 创建 BoxApi 实例
|
||||
const boxApi = new BoxApi();
|
||||
|
||||
@@ -208,6 +223,66 @@ const totalItems = ref(0);
|
||||
const displayTotalItems = ref(0); // 用于展示的数字
|
||||
const cameras = ref([]);
|
||||
|
||||
const formatDateTimeToISO = (datetime) => {
|
||||
return new Date(datetime).toISOString().replace('.000', '');
|
||||
};
|
||||
|
||||
|
||||
const isExporting = ref(false);
|
||||
// 导出数据方法
|
||||
const exportData = async () => {
|
||||
if (isExporting.value) return; // 如果已经在导出,直接返回,避免重复点击
|
||||
|
||||
isExporting.value = true; // 设置为正在导出状态
|
||||
try {
|
||||
const allData = [];
|
||||
let page = 1;
|
||||
let totalPages = 1;
|
||||
const limit = 500;
|
||||
|
||||
// 循环分页请求所有数据
|
||||
do {
|
||||
const { results, count } = await boxApi.getEventsByParams(
|
||||
token.value,
|
||||
limit,
|
||||
page,
|
||||
filterParams.timeBefore ? formatDateTimeToISO(filterParams.timeBefore) : null,
|
||||
filterParams.timeAfter ? formatDateTimeToISO(filterParams.timeAfter) : null,
|
||||
filterParams.types,
|
||||
filterParams.cameraId,
|
||||
filterParams.status
|
||||
);
|
||||
|
||||
// 格式化数据
|
||||
const formattedData = results.map(item => ({
|
||||
'告警编号': item.id,
|
||||
'告警类型': typeMapping[item.types] || item.types,
|
||||
'告警位置': item.camera ? item.camera.name : '',
|
||||
'告警时间': formatDateTime(item.ended_at),
|
||||
'告警状态': statusMapping[item.status] || item.status
|
||||
}));
|
||||
|
||||
allData.push(...formattedData);
|
||||
totalPages = Math.ceil(count / limit);
|
||||
page += 1;
|
||||
} while (page <= totalPages);
|
||||
|
||||
// 使用 PapaParse 将 JSON 转换为 CSV,指定列标题
|
||||
const csv = Papa.unparse(allData, {
|
||||
columns: ['告警编号', '告警类型', '告警位置', '告警时间', '告警状态']
|
||||
});
|
||||
|
||||
// 添加 BOM,确保中文编码正确
|
||||
const blob = new Blob(["\uFEFF" + csv], { type: 'text/csv;charset=utf-8;' });
|
||||
saveAs(blob, 'Alert_data.csv');
|
||||
} catch (error) {
|
||||
console.error("导出数据错误:", error);
|
||||
}finally {
|
||||
isExporting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
@@ -240,6 +315,8 @@ const filterParams = reactive({
|
||||
types: null,
|
||||
timeAfter: null,
|
||||
timeBefore: null,
|
||||
cameraId: null,
|
||||
status: null,
|
||||
});
|
||||
|
||||
|
||||
@@ -273,9 +350,10 @@ const handleFilter = async () => {
|
||||
const types = filterParams.types || null;
|
||||
// const timeAfter = filterParams.timeAfter ? new Date(filterParams.timeAfter).toISOString() : null;
|
||||
// const timeBefore = filterParams.timeBefore ? new Date(filterParams.timeBefore).toISOString() : null;
|
||||
const timeAfter = filterParams.timeAfter ? formatDateTime(new Date(filterParams.timeAfter)) : null;
|
||||
const timeBefore = filterParams.timeBefore ? formatDateTime(new Date(filterParams.timeBefore)) : null;
|
||||
const timeAfter = filterParams.timeAfter ? formatDateTimeToISO(new Date(filterParams.timeAfter)) : null;
|
||||
const timeBefore = filterParams.timeBefore ? formatDateTimeToISO(new Date(filterParams.timeBefore)) : null;
|
||||
const cameraId = filterParams.cameraId || null;
|
||||
const status = filterParams.status || null;
|
||||
|
||||
const { results, count } = await boxApi.getEventsByParams(
|
||||
token.value,
|
||||
@@ -284,7 +362,8 @@ const handleFilter = async () => {
|
||||
timeBefore,
|
||||
timeAfter,
|
||||
types,
|
||||
cameraId
|
||||
cameraId,
|
||||
status
|
||||
);
|
||||
tableData.value = results;
|
||||
totalItems.value = count;
|
||||
@@ -298,6 +377,7 @@ const handleReset = () => {
|
||||
filterParams.timeAfter = null;
|
||||
filterParams.timeBefore = null;
|
||||
filterParams.cameraId = null;
|
||||
filterParams.status = null;
|
||||
fetchEvents(); // 重置筛选条件后,重新获取所有告警数据
|
||||
};
|
||||
|
||||
@@ -386,7 +466,7 @@ const closeDialog = () => {
|
||||
// 分页处理
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page;
|
||||
if (filterParams.types || filterParams.timeAfter || filterParams.timeBefore) {
|
||||
if (filterParams.types || filterParams.timeAfter || filterParams.timeBefore || filterParams.cameraId || filterParams.status) {
|
||||
handleFilter();
|
||||
} else {
|
||||
fetchEvents();
|
||||
@@ -434,7 +514,9 @@ onMounted(async () => {
|
||||
.alert-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f5f7fa;
|
||||
background-color: #001529;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* .top-pan {
|
||||
@@ -453,7 +535,7 @@ onMounted(async () => {
|
||||
border-radius: 15px;
|
||||
} */
|
||||
|
||||
.middle-row {
|
||||
/* .middle-row {
|
||||
min-height: 5vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -461,14 +543,43 @@ onMounted(async () => {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(to bottom left, rgb(150, 151, 243), rgb(215, 214, 250));
|
||||
/* border-radius: 8px; */
|
||||
border-radius: 8px;
|
||||
} */
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
height: 15vh;
|
||||
padding: 0px 0;
|
||||
margin: 5vh 6vw 5vh 1vw;
|
||||
gap: 8px;
|
||||
/* font-weight: bold; */
|
||||
font-size: 15px;
|
||||
background-color: rgb(251, 254, 255);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
max-width: 100%;
|
||||
height: 80%;
|
||||
/* min-height: 50vh; */
|
||||
/* max-height: 70vh; */
|
||||
overflow-x: auto;
|
||||
margin: 0vh 6vw 0vh 1vw;
|
||||
padding: 0;
|
||||
overflow-y: hidden;
|
||||
border-radius: 8px 8px 0 0;
|
||||
background-color: white;
|
||||
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
background-color: #e9eefc;
|
||||
padding-left: 20px;
|
||||
margin: 0vh 6vw 5vh 1vw;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
@@ -477,8 +588,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
::v-deep .el-table th.el-table__cell {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
background-color: #e9eefca4;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.event-media {
|
||||
@@ -522,12 +633,7 @@ onMounted(async () => {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
background-color: #e8e9e4;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
|
||||
::v-deep .pagination-container .el-pagination__total,
|
||||
::v-deep .pagination-container .el-pagination__goto,
|
||||
@@ -541,15 +647,4 @@ onMounted(async () => {
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
height: 80px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
gap: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,124 +1,702 @@
|
||||
<template>
|
||||
<div class="alert-container" >
|
||||
<el-row class="bottom-pan">
|
||||
<el-col class="panel-bottom">
|
||||
<Cameras/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="static-container">
|
||||
|
||||
<el-row class="top-pan">
|
||||
<el-col :sm="24" :md="12" class="panel-top-left">
|
||||
<statistics />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" class="panel-top-right">
|
||||
<alertChart />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="search-row">
|
||||
<el-date-picker v-model="filterParams.timeAfter" :teleported="false" type="datetime" placeholder="请选择开始时间"
|
||||
prefix-icon="CaretBottom" popper-class="popperClass"></el-date-picker>
|
||||
<el-date-picker v-model="filterParams.timeBefore" :teleported="false" type="datetime" placeholder="请选择结束时间"
|
||||
prefix-icon="CaretBottom" popper-class="popperClass"></el-date-picker>
|
||||
<el-button type="primary" @click="handleFilter" class="alert-bt">查询</el-button>
|
||||
<el-button type="primary" @click="handleReset" class="alert-bt">清空</el-button>
|
||||
</div>
|
||||
|
||||
<div class="second-row">
|
||||
<el-row class="top-part">
|
||||
<el-col :sm="24" :md="6">
|
||||
<div class="camera-stats">
|
||||
<div id="camera-chart-container" class="camera-status-chart"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="6">
|
||||
<div class="event-stats">
|
||||
<div id="event-chart-container" class="event-status-chart"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<div class="part-three">
|
||||
<AlertChart />
|
||||
</div>
|
||||
</el-col>
|
||||
<!-- <el-col :sm="24" :md="6" >
|
||||
<div class="part-four">
|
||||
|
||||
</div>
|
||||
</el-col> -->
|
||||
</el-row>
|
||||
|
||||
<el-row class="bottom-part">
|
||||
<el-col :span="24">
|
||||
<div class="bottom-row">
|
||||
<div id="code-chart-container" class="code-status-chart"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- <el-row>
|
||||
<el-col :span="24">
|
||||
<div class="statistics-container">
|
||||
<p v-for="(name, code) in typeMapping" :key="code">
|
||||
{{ name }}: {{ typeCounts[code] || 0 }} 次
|
||||
</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row> -->
|
||||
|
||||
<!--<p v-for="(name, code) in typeMapping" :key="code">
|
||||
{{ name }}: 总数 {{ typeCounts[code]?.total || 0 }} 次, 处理中 {{ typeCounts[code]?.pending || 0 }} 次, 已处理 {{
|
||||
typeCounts[code]?.closed || 0 }} 次
|
||||
</p> -->
|
||||
<!-- 摄像总数{{ cameraCount }}
|
||||
摄像在线{{ cameraOnlineCount }}
|
||||
摄像离线{{ cameraOfflineCount }}
|
||||
事件总数{{ eventCount }}
|
||||
处理中{{ pendingEventCount }}
|
||||
已处理{{ closedEventCount }} -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted,computed, onBeforeUnmount} from 'vue';
|
||||
import Statistics from '@/components/Statistics.vue';
|
||||
import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import * as echarts from 'echarts';
|
||||
import AlertChart from '@/components/AlertChart.vue';
|
||||
import Cameras from '@/components/Cameras.vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
||||
|
||||
// 创建 BoxApi 实例
|
||||
const boxApi = new BoxApi();
|
||||
const apiInstance = new BoxApi();
|
||||
const typeMapping = reactive({});
|
||||
const token = ref(null);
|
||||
// const scale = ref(1);
|
||||
const typeCounts = reactive({});
|
||||
const filterParams = reactive({
|
||||
timeAfter: null,
|
||||
timeBefore: null,
|
||||
});
|
||||
|
||||
// const scaleStyle = computed(() => ({
|
||||
// transform: `scale(${scale.value})`,
|
||||
// transformOrigin: 'top left',
|
||||
// width: `${100 / scale.value}%`,
|
||||
// }));
|
||||
const xData = ref([]);
|
||||
const totalData = ref([]);
|
||||
const pendingData = ref([]);
|
||||
|
||||
// const handleResize = () => {
|
||||
// const clientWidth = document.documentElement.clientWidth;
|
||||
// const scaleFactor = clientWidth / 1920;
|
||||
// scale.value = scaleFactor < 1 ? scaleFactor : 1;
|
||||
// };
|
||||
const cameraCount = ref(0);
|
||||
const cameraOfflineCount = ref(0);
|
||||
const cameraOnlineCount = ref(0);
|
||||
|
||||
// 获取类型映射
|
||||
const eventCount = ref(0);
|
||||
const pendingEventCount = ref(0);
|
||||
const closedEventCount = ref(0);
|
||||
|
||||
const formatDateTimeToISO = (datetime) => {
|
||||
return new Date(datetime).toISOString().replace('.000', '');
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
// filterParams.types = null;
|
||||
filterParams.timeAfter = null;
|
||||
filterParams.timeBefore = null;
|
||||
// filterParams.cameraId = null;
|
||||
// filterParams.status = null;
|
||||
handleFilter();
|
||||
};
|
||||
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
|
||||
const fetchAllCounts = async (timeAfter, timeBefore) => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter);
|
||||
eventCount.value = totalResponse.count;
|
||||
|
||||
// const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
|
||||
// pendingEventCount.value = pendingResponse.count;
|
||||
const { count: pendingCount } = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
|
||||
pendingEventCount.value = pendingCount;
|
||||
|
||||
|
||||
const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'closed');
|
||||
closedEventCount.value = closedResponse.count;
|
||||
} catch (error) {
|
||||
console.error('Error fetching all counts:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
cameraCount.value = firstResponse.count;
|
||||
// console.log("总数》》》》》》》》》》》》》", cameraCount.value)
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
|
||||
const total = cameraCount.value;
|
||||
while (offset + limit < total) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
|
||||
allCameras.forEach((camera) => {
|
||||
if (camera.status === 'online') {
|
||||
cameraOnlineCount.value++;
|
||||
} else if (camera.status === 'offline') {
|
||||
cameraOfflineCount.value++;
|
||||
} else {
|
||||
exceptionCount.value++;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching cameras:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 获取告警类型并初始化每种类型的count
|
||||
const fetchTypeMapping = async (token) => {
|
||||
try {
|
||||
const algorithms = await boxApi.getAlgorithms(token); // 使用 BoxApi 的 getAlgorithms 方法
|
||||
const algorithms = await apiInstance.getAlgorithms(token);
|
||||
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
||||
|
||||
algorithms.forEach((algorithm) => {
|
||||
typeMapping[algorithm.code_name] = algorithm.name;
|
||||
typeCounts[algorithm.code_name] = { total: 0, pending: 0, closed: 0 };
|
||||
});
|
||||
|
||||
additionalMappings.forEach((item) => {
|
||||
typeMapping[item.code_name] = item.name;
|
||||
typeCounts[item.code_name] = { total: 0, pending: 0, closed: 0 };
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching algorithms:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTypeCounts = async (timeAfter, timeBefore) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const tempCounts = []; // 临时数组用于存储类型和总数
|
||||
|
||||
// 清空数据以确保每次查询更新时数据正确
|
||||
xData.value = [];
|
||||
totalData.value = [];
|
||||
pendingData.value = [];
|
||||
|
||||
// 遍历每种类型,根据 code_name 获取总数
|
||||
for (const code in typeMapping) {
|
||||
try {
|
||||
const { count: totalCount } = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, code);
|
||||
tempCounts.push({ code, name: typeMapping[code], total: totalCount });
|
||||
} catch (error) {
|
||||
console.error(`Error fetching total count for type ${code}:`, error);
|
||||
tempCounts.push({ code, name: typeMapping[code], total: 0 }); // 如果有错误,默认总数为 0
|
||||
}
|
||||
}
|
||||
|
||||
// 根据总数降序排序
|
||||
tempCounts.sort((a, b) => b.total - a.total);
|
||||
|
||||
// 更新 xData、totalData 和 pendingData 按排序后的顺序
|
||||
xData.value = tempCounts.map(item => item.name); // 排序后的名称顺序
|
||||
totalData.value = tempCounts.map(item => item.total); // 排序后的总数顺序
|
||||
|
||||
// 获取每种类型的处理中数量,并按排序顺序赋值到 pendingData
|
||||
for (const item of tempCounts) {
|
||||
try {
|
||||
const { count: pendingCount } = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, item.code, null, 'pending');
|
||||
pendingData.value.push(pendingCount);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching pending count for type ${item.code}:`, error);
|
||||
pendingData.value.push(0); // 如果有错误,默认处理中数量为 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilter = async () => {
|
||||
const timeAfter = filterParams.timeAfter ? formatDateTimeToISO(filterParams.timeAfter) : null;
|
||||
const timeBefore = filterParams.timeBefore ? formatDateTimeToISO(filterParams.timeBefore) : null;
|
||||
|
||||
console.log('timeAfter:', timeAfter);
|
||||
await fetchTypeCounts(timeAfter, timeBefore);
|
||||
await fetchAllCounts(timeAfter, timeBefore);
|
||||
|
||||
|
||||
renderChart();
|
||||
renderCameraChart();
|
||||
renderEventChart();
|
||||
};
|
||||
|
||||
|
||||
const renderCameraChart = () => {
|
||||
const chartDom = document.getElementById('camera-chart-container');
|
||||
const chart = echarts.init(chartDom);
|
||||
|
||||
const chartData = { ONE: cameraOnlineCount.value, TWO: cameraOfflineCount.value, num: cameraCount.value };
|
||||
const seriesData = [];
|
||||
const legendData = [];
|
||||
const nameList = [
|
||||
{ key: 'ONE', text: '在线', color: '#243eff' },
|
||||
{ key: 'TWO', text: '离线', color: '#2490ff' },
|
||||
// { key: 'num', text: '总数' },
|
||||
];
|
||||
|
||||
nameList.forEach((item) => {
|
||||
seriesData.push({
|
||||
value: chartData[item.key],
|
||||
name: item.text,
|
||||
itemStyle: { color: item.color },
|
||||
});
|
||||
legendData.push(item.text); // 直接添加到 legendData 数组中
|
||||
});
|
||||
|
||||
const option = {
|
||||
backgroundColor: '#001529',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: ({ name, value, percent }) => {
|
||||
const color = nameList.find((item) => item.text === name).color;
|
||||
return `<span style="color:${color};">●</span> ${name} 数量: ${value}<br><span style="color:${color};">●</span> ${name} 比例: ${percent}%`;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical', // 设置纵向排列
|
||||
top: '8%',
|
||||
left: '10%',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
},
|
||||
icon: 'circle',
|
||||
data: legendData, // 使用 legendData 数组作为数据
|
||||
formatter: (name) => {
|
||||
const item = nameList.find((n) => n.text === name);
|
||||
return `${name} ${chartData[item.key] || 0}`;
|
||||
},
|
||||
rich: nameList.reduce((acc, item) => {
|
||||
acc[item.key] = {
|
||||
color: item.color,
|
||||
fontSize: 14,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
color: ['#243eff', '#2490ff', '#30f3e3'],
|
||||
series: [
|
||||
{
|
||||
name: '摄像头状态',
|
||||
type: 'pie',
|
||||
center: ['50%', '60%'],
|
||||
radius: ['40%', '50%'],
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 15,
|
||||
formatter: () => `摄像状态`,
|
||||
},
|
||||
labelLine: { show: false },
|
||||
data: seriesData,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
};
|
||||
|
||||
const renderEventChart = () => {
|
||||
const chartDom = document.getElementById('event-chart-container');
|
||||
const chart = echarts.init(chartDom);
|
||||
|
||||
const chartData = { ONE: pendingEventCount.value, TWO: closedEventCount.value, num: eventCount.value };
|
||||
const seriesData = [];
|
||||
const legendData = [];
|
||||
const nameList = [
|
||||
{ key: 'ONE', text: '处理中', color: '#243eff' },
|
||||
{ key: 'TWO', text: '已处理', color: '#2490ff' },
|
||||
];
|
||||
|
||||
nameList.forEach((item) => {
|
||||
seriesData.push({
|
||||
value: chartData[item.key],
|
||||
name: item.text,
|
||||
itemStyle: { color: item.color },
|
||||
});
|
||||
legendData.push(item.text);
|
||||
});
|
||||
|
||||
const option = {
|
||||
backgroundColor: '#001529',
|
||||
color: ['#243eff', '#2490ff', '#30f3e3'],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: ({ name, value, percent }) => {
|
||||
const color = nameList.find((item) => item.text === name).color;
|
||||
return `<span style="color:${color};">●</span> ${name} 数量: ${value}<br><span style="color:${color};">●</span> ${name} 比例: ${percent}%`;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
top: '5%',
|
||||
left: '10%',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
},
|
||||
icon: 'circle',
|
||||
data: legendData,
|
||||
formatter: (name) => {
|
||||
const item = nameList.find((n) => n.text === name);
|
||||
return `${name} ${chartData[item.key] || 0}`;
|
||||
}
|
||||
},
|
||||
rich: nameList.reduce((acc, item) => {
|
||||
acc[item.key] = {
|
||||
color: item.color,
|
||||
fontSize: 14,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
series: [
|
||||
{
|
||||
name: '',
|
||||
type: 'pie',
|
||||
center: ['50%', '60%'],
|
||||
radius: ['40%', '50%'],
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 15,
|
||||
formatter: () => `事件概览 `,
|
||||
},
|
||||
labelLine: { show: false },
|
||||
data: seriesData,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const renderChart = () => {
|
||||
const chartDom = document.getElementById('code-chart-container');
|
||||
const chart = echarts.init(chartDom);
|
||||
|
||||
const option = {
|
||||
backgroundColor: '#001529',
|
||||
title: {
|
||||
text: ` 算法摄像头分布`, // 添加标题文本
|
||||
left: '2%', // 设置标题位置为居中
|
||||
top: '10%', // 设置标题距离顶部的位置
|
||||
textStyle: {
|
||||
color: '#5BFCF4', // 标题颜色
|
||||
fontSize: 16, // 标题字体大小
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '10%',
|
||||
bottom: '5%',
|
||||
right: '5%',
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: xData.value, // 使用排序后的 xData
|
||||
axisTick: { alignWithLabel: true },
|
||||
axisLine: { show: false },
|
||||
axisLabel: { textStyle: { color: '#ddd' }, margin: 30 },
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
show: false,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '上部圆 - 总数',
|
||||
type: 'pictorialBar',
|
||||
silent: true,
|
||||
symbolSize: [40, 10],
|
||||
symbolOffset: [0, -6],
|
||||
symbolPosition: 'end',
|
||||
z: 12,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
fontSize: 15,
|
||||
fontWeight: 'bold',
|
||||
color: '#5BFCF4',
|
||||
},
|
||||
color: '#5BFCF4',
|
||||
data: totalData.value,
|
||||
},
|
||||
{
|
||||
name: '底部圆 - 处理中',
|
||||
type: 'pictorialBar',
|
||||
silent: true,
|
||||
symbolSize: [40, 10],
|
||||
symbolOffset: [0, 7],
|
||||
z: 12,
|
||||
color: '#FE9C5A',
|
||||
data: pendingData.value,
|
||||
},
|
||||
{
|
||||
name: '内层波浪',
|
||||
type: 'pictorialBar',
|
||||
silent: true,
|
||||
symbolSize: [50, 15],
|
||||
symbolOffset: [0, 12],
|
||||
z: 10,
|
||||
itemStyle: {
|
||||
color: 'transparent',
|
||||
borderColor: '#5BFCF4',
|
||||
borderWidth: 8,
|
||||
},
|
||||
data: totalData.value,
|
||||
},
|
||||
{
|
||||
name: '外层波浪',
|
||||
type: 'pictorialBar',
|
||||
silent: true,
|
||||
symbolSize: [70, 20],
|
||||
symbolOffset: [0, 18],
|
||||
z: 10,
|
||||
itemStyle: {
|
||||
color: 'transparent',
|
||||
borderColor: 'rgba(91,252,244,0.5)',
|
||||
borderWidth: 5,
|
||||
},
|
||||
data: totalData.value,
|
||||
},
|
||||
{
|
||||
name: '设备数量 - 总数',
|
||||
type: 'bar',
|
||||
barWidth: '40',
|
||||
barGap: '10%',
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 0.7, [
|
||||
{ offset: 0, color: 'rgba(210,210,210,0.3)' },
|
||||
{ offset: 1, color: '#5BFCF4' },
|
||||
]),
|
||||
opacity: 0.8,
|
||||
},
|
||||
data: totalData.value,
|
||||
},
|
||||
{
|
||||
name: '告警数量 - 处理中',
|
||||
type: 'bar',
|
||||
barWidth: 40,
|
||||
z: 12,
|
||||
barGap: '-100%',
|
||||
itemStyle: {
|
||||
opacity: 0.7,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#EB3B5A' },
|
||||
{ offset: 1, color: '#FE9C5A' },
|
||||
]),
|
||||
},
|
||||
data: pendingData.value,
|
||||
},
|
||||
{
|
||||
name: '上部圆 - 总数',
|
||||
type: 'pictorialBar',
|
||||
silent: true,
|
||||
symbolSize: [40, 10],
|
||||
symbolOffset: [0, -6],
|
||||
symbolPosition: 'end',
|
||||
z: 12,
|
||||
color: '#5BFCF4',
|
||||
data: totalData.value,
|
||||
},
|
||||
{
|
||||
name: '底部圆 - 处理中',
|
||||
type: 'pictorialBar',
|
||||
silent: true,
|
||||
symbolSize: [40, 10],
|
||||
symbolOffset: [0, 7],
|
||||
z: 12,
|
||||
color: '#FE9C5A',
|
||||
data: pendingData.value,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
token.value = localStorage.getItem('alertToken');
|
||||
await fetchTypeMapping(token.value);
|
||||
// handleResize();
|
||||
// window.addEventListener('resize', handleResize);
|
||||
const token = localStorage.getItem('alertToken');
|
||||
|
||||
|
||||
await fetchTypeMapping(token);
|
||||
await fetchTypeCounts();
|
||||
await fetchAllCounts();
|
||||
await fetchCameras();
|
||||
|
||||
renderChart();
|
||||
renderCameraChart();
|
||||
renderEventChart();
|
||||
|
||||
window.addEventListener('resize', () => echarts.getInstanceByDom(document.getElementById('chart-container')).resize());
|
||||
});
|
||||
|
||||
// onBeforeUnmount(() => {
|
||||
// window.removeEventListener('resize', handleResize);
|
||||
// });
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', () => echarts.getInstanceByDom(document.getElementById('chart-container')).resize());
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alert-container {
|
||||
/* transform: scale(0.97); */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f5f7fa;
|
||||
overflow-x: hidden;
|
||||
height: 100vh;
|
||||
.static-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #001529;
|
||||
/* background: linear-gradient(to right bottom, rgb(0, 21, 41), rgba(3, 55, 153, 0.3)); */
|
||||
/* background: repeating-radial-gradient(circle, rgb(7, 7, 7), rgba(14, 14, 211, 0.5) 20px, rgba(3, 0, 151, 0.979) 100px); */
|
||||
/* background: repeating-linear-gradient(45deg, rgba(0, 21, 41, 0.9), rgba(12, 203, 209, 0.5) 20px); */
|
||||
/* background:
|
||||
radial-gradient(circle at 0% 0%, rgba(0, 21, 41,1), rgba(12, 203, 209, 0.2)),
|
||||
radial-gradient(circle at 100% 100%, rgba(12, 203, 209, 0.3), rgba(0, 21, 41, 1)),
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.5), transparent); */
|
||||
|
||||
/* box-sizing: border-box; */
|
||||
}
|
||||
|
||||
.top-pan {
|
||||
/* padding: 10px; */
|
||||
/* margin-bottom: 10px; */
|
||||
.search-row {
|
||||
width: 82vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 15vh;
|
||||
/* background-color: #4587dd; */
|
||||
background: repeating-linear-gradient(-45deg, rgba(0, 21, 41), rgba(58, 61, 214, 0.3) 100px);
|
||||
gap: 1vw;
|
||||
padding: 0 2vw;
|
||||
margin: 2vh 0vw 2vh 3vw;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.second-row {
|
||||
/* display: flex; */
|
||||
/* gap: 5px; */
|
||||
/* background-color: #fff; */
|
||||
/* background-color: #1E2E4A; */
|
||||
/* overflow: hidden; */
|
||||
/* height: 55vh; */
|
||||
/* max-height: 450px; */
|
||||
/* padding-left: 1vh; */
|
||||
/* padding-right:1vh ; */
|
||||
/* overflow: hidden; */
|
||||
height: 56vh;
|
||||
margin-top: 60px;
|
||||
/* border: 1px solid #1E2E4A; */
|
||||
|
||||
}
|
||||
.bottom-pan{
|
||||
margin: 0;
|
||||
/* flex-direction: column; */
|
||||
/* align-items: center; */
|
||||
/* gap: 10px; */
|
||||
padding: 0;
|
||||
height: 33vh;
|
||||
box-sizing: border-box;
|
||||
margin-left: 3vw;
|
||||
width: 86vw;
|
||||
height: 70vh;
|
||||
border-radius: 10px;
|
||||
background-color: #001529;
|
||||
|
||||
}
|
||||
|
||||
.panel-top-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.top-part {
|
||||
margin-left: 1vw;
|
||||
height: 30vh;
|
||||
/* border: solid 1px #70ce18; */
|
||||
}
|
||||
|
||||
.panel-top-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* gap: 20px; */
|
||||
}
|
||||
|
||||
.panel-bottom{
|
||||
margin: 0;
|
||||
.camera-status-chart,
|
||||
.event-status-chart {
|
||||
height: 30vh;
|
||||
width: 20vw;
|
||||
padding: 0;
|
||||
margin: 1vh 0vw 0 0vw;
|
||||
padding: 0;
|
||||
border: none;
|
||||
/* border: solid 1px #ce1827; */
|
||||
}
|
||||
|
||||
.part-three,
|
||||
.part-four {
|
||||
height: 30vh;
|
||||
width: 41vw;
|
||||
/* padding: 2px 0; */
|
||||
margin: 1vh 0vw 0 0;
|
||||
/* border: solid 1px #a418ce; */
|
||||
/* margin: 1vh 1vw 0 0; */
|
||||
}
|
||||
|
||||
/* .event-status-chart{
|
||||
height: 30vh;
|
||||
width: 24vw;
|
||||
padding: 0;
|
||||
margin: 1vh 1vh 0 0;
|
||||
} */
|
||||
|
||||
.camera-stats,
|
||||
.event-stats {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #001529;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* .bottom-part{
|
||||
box-sizing: border-box;
|
||||
height: 45vh;
|
||||
width: 86vw;
|
||||
|
||||
} */
|
||||
|
||||
.bottom-row {
|
||||
/* margin: 10px; */
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.code-status-chart {
|
||||
width: 84vw;
|
||||
height: 40vh;
|
||||
padding: 0vh 1vw 0 1vw;
|
||||
/* margin: 0vh 1vw; */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,411 +1,345 @@
|
||||
<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="list-view">
|
||||
<div class="background-overlay"></div>
|
||||
<div class="container">
|
||||
<!-- 头栏 -->
|
||||
<div class="header">
|
||||
<div class="title-left">
|
||||
<div small-bg>
|
||||
<dv-decoration8 :reverse="true" style="width:28vw;height:60px;" />
|
||||
</div>
|
||||
<div small-bg>
|
||||
<dv-decoration3 style="width:250px;height:30px;" />
|
||||
</div>
|
||||
</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 class="line-title">
|
||||
<div text-2xl pt10>
|
||||
<div small-bg>
|
||||
<dv-decoration-11 class="custom-decoration" style="width:25vw;height:70px;">
|
||||
<div color-green font-700 bg="~ dark/0">
|
||||
<dv-decoration7 style="width:20vw;height:30px;">
|
||||
<div color-white font-400 >
|
||||
告警数据面板
|
||||
</div>
|
||||
</dv-decoration7>
|
||||
</div>
|
||||
</dv-decoration-11>
|
||||
</div>
|
||||
</div>
|
||||
<div small-bg>
|
||||
<dv-decoration5 :dur="2" style="width:25vw;height:80px;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="title-right">
|
||||
<div small-bg class="first-row">
|
||||
<dv-decoration8 style="width:28vw;height:60px;" />
|
||||
</div>
|
||||
<div small-bg class="second-row">
|
||||
<dv-decoration3 style="width:250px;height:30px;" />
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<div class="main-section">
|
||||
<!-- 左侧区域 -->
|
||||
<div class="left-section">
|
||||
<!-- <div class="section top-left corner-style" >左上
|
||||
<div class="section hiden"></div>
|
||||
</div>
|
||||
<div class="section middle-left corner-style">左中
|
||||
<div class="section hiden"></div>
|
||||
</div>
|
||||
<div class="section bottom-left corner-style">左下
|
||||
<div class="section hiden"></div>
|
||||
</div> -->
|
||||
<dv-border-box-13 title="告警数据概览(数据计算数字)" class="section top-left">
|
||||
<LeftTop />
|
||||
</dv-border-box-13>
|
||||
<dv-border-box-13 title="点位告警数量(不同点位的数量)" class="section middle-left">不同点位告警的数量
|
||||
<LeftMiddle/>
|
||||
</dv-border-box-13>
|
||||
<!-- <dv-border-box-13 title="今日告警列表(告警详情)" class="section bottom-left ">今日告警列表(告警详情)
|
||||
|
||||
</dv-border-box-13> -->
|
||||
</div>
|
||||
|
||||
<img v-if="!camera.playing && camera.snapshot" :src="camera.snapshot" alt="camera snapshot"
|
||||
class="camera-snapshot" />
|
||||
<!-- 中部区域 -->
|
||||
<div class="center-section">
|
||||
<dv-border-box8 class="center-top">
|
||||
<CenterTop/>
|
||||
<!-- <dv-border-box8 class="center-top-header">警戒画面</dv-border-box8>
|
||||
<div class="center-top-grids">
|
||||
<div class="grid-item">栅格左上</div>
|
||||
<div class="grid-item">栅格右上</div>
|
||||
<div class="grid-item">栅格左下</div>
|
||||
<div class="grid-item">栅格右下</div>
|
||||
</div> -->
|
||||
</dv-border-box8>
|
||||
<div class="center-bottom">
|
||||
<!-- <dv-border-box-13 class="center-bottom-left">中下左</dv-border-box-13> -->
|
||||
<dv-border-box-13 class="center-bottom-right">告警数量分布情况
|
||||
<CenterBottom/>
|
||||
</dv-border-box-13>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 播放视频流的 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 class="right-section">
|
||||
<dv-border-box-13 class="section top-right corner-style">时间段告警总数分布
|
||||
<RightTop/>
|
||||
</dv-border-box-13>
|
||||
<!-- <dv-border-box-13 class="section middle-right corner-style">告警数量分布</dv-border-box-13> -->
|
||||
<dv-border-box-13 class="section bottom-right corner-style">告警种类划分
|
||||
<LeftBottom/>
|
||||
</dv-border-box-13>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick,computed } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick, computed } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
|
||||
import LeftTop from '@/components/Max/LeftTop.vue';
|
||||
import LeftBottom from '@/components/Max/LeftBottom.vue';
|
||||
import LeftMiddle from '@/components/Max/LeftMiddle.vue';
|
||||
import RightTop from '@/components/Max/RightTop.vue';
|
||||
import CenterBottom from '@/components/Max/CenterBottom.vue';
|
||||
import CenterTop from '@/components/Max/CenterTop.vue';
|
||||
// import '/src/assets/viewListStyle.css'
|
||||
|
||||
// 存储所有摄像头列表
|
||||
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 {
|
||||
.list-view {
|
||||
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;
|
||||
padding-bottom: 10vh;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 每个摄像头项目的样式 */
|
||||
.camera-item {
|
||||
margin-bottom: 10px;
|
||||
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;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
padding: 4vh 10vw 10vh 7vw;
|
||||
/* background-color: rgb(121, 184, 243); */
|
||||
background-color: #001529;
|
||||
/* background-image: url('/bg05.png'); */
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
/* background: radial-gradient(circle, rgb(24, 64, 197), rgb(0, 7, 60)); */
|
||||
position: relative;
|
||||
/* padding: 10px; */
|
||||
color: black;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.stream-control {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.custom-decoration{
|
||||
color: #70e5fa;;
|
||||
font-weight: bold;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
/* .stream-control p {
|
||||
color: #f5f7fa;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
font-weight: bold;
|
||||
|
||||
/* .background-overlay {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: url('/bg01.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
opacity: 0.8;
|
||||
z-index: 0;
|
||||
|
||||
} */
|
||||
|
||||
.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 {
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
width: 80vw;
|
||||
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 {
|
||||
gap: 1vh;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
/* 为了在背景下显示 */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 12vh;
|
||||
width: 81vw;
|
||||
/* background-color: #000080; */
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 12vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.line-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* gap: 2vh; */
|
||||
}
|
||||
|
||||
.title-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title-right {
|
||||
display: flex;
|
||||
flex-direction: column; /* 设置为垂直布局 */
|
||||
}
|
||||
|
||||
.first-row {
|
||||
display: flex;
|
||||
justify-content: flex-start; /* 第一行内容靠左 */
|
||||
}
|
||||
|
||||
.second-row {
|
||||
display: flex;
|
||||
justify-content: flex-end; /* 第二行内容靠右 */
|
||||
|
||||
}
|
||||
|
||||
.main-section {
|
||||
height: 72vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1vw;
|
||||
}
|
||||
|
||||
.left-section,
|
||||
.right-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 22vw;
|
||||
height: 70vh;
|
||||
gap: 2vh;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.bottom-left,
|
||||
.top-right,
|
||||
/* .middle-right, */
|
||||
.bottom-right {
|
||||
color: white;
|
||||
text-align: center;
|
||||
width: 22vw;
|
||||
height: 22vh;
|
||||
/* background-color: rgba(255, 255, 255, 0.1); */
|
||||
/* border: 3px solid rgba(0, 255, 255, 0.5); */
|
||||
/* box-shadow: 0 2px 5px rgba(221, 204, 204, 0.5); */
|
||||
}
|
||||
|
||||
.top-left{
|
||||
position: relative;
|
||||
padding: 1vh 1vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
width: 20vw;
|
||||
height: 20vh;
|
||||
}
|
||||
|
||||
.middle-left{
|
||||
color: white;
|
||||
text-align: center;
|
||||
width: 22vw;
|
||||
height: 46vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.top-right{
|
||||
width: 22vw;
|
||||
height: 46vh;
|
||||
display: flex;
|
||||
/* padding: 1.5vh; */
|
||||
}
|
||||
|
||||
.bottom-left{
|
||||
/* color: white; */
|
||||
/* position: relative; */
|
||||
padding-left: 0.5vw;
|
||||
display: flex;
|
||||
/* justify-content: center; */
|
||||
/* align-items: center; */
|
||||
/* width: 20vw; */
|
||||
/* height: 20vh; */
|
||||
}
|
||||
|
||||
/* .top-left, .top-right {
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
|
||||
|
||||
.bottom-left, .bottom-right {
|
||||
margin-top: 1vh;
|
||||
} */
|
||||
|
||||
.center-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 35vw;
|
||||
height: 70vh;
|
||||
gap: 1vh;
|
||||
}
|
||||
|
||||
.center-top {
|
||||
height: 47vh;
|
||||
/* background-color: #555; */
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* .center-top-header {
|
||||
height: 3vh;
|
||||
width: 35vw;
|
||||
text-align: center;
|
||||
line-height: 3vh;
|
||||
margin-bottom: 1vh;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.center-top-grids {
|
||||
display: grid;
|
||||
grid-template-columns: 17vw 17vw;
|
||||
gap: 2vh 1vw;
|
||||
}
|
||||
.grid-item {
|
||||
width: 17vw;
|
||||
height: 20vh;
|
||||
background-color: #777;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
} */
|
||||
|
||||
.center-bottom {
|
||||
display: flex;
|
||||
gap: 1vw;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* 播放按钮 */
|
||||
.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;
|
||||
.center-bottom-left,
|
||||
.center-bottom-right {
|
||||
width: 35vw;
|
||||
height: 22vh;
|
||||
/* background-color: #444; */
|
||||
color: white;
|
||||
text-align: center;
|
||||
/* margin-top: 1vh; */
|
||||
}
|
||||
|
||||
.play-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,365 +1,180 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTab" class="tab-div">
|
||||
<el-tab-pane label="所有" name="all"></el-tab-pane>
|
||||
<el-tab-pane label="今天" name="today"></el-tab-pane>
|
||||
<el-tab-pane label="7天" name="week"></el-tab-pane>
|
||||
<el-tab-pane label="30天" name="month"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="settings-container">
|
||||
<el-row class="popup-row">
|
||||
<el-checkbox v-model="isPopupEnabled" @change="handleCheckboxChange" style="color: aliceblue;">
|
||||
开启弹窗
|
||||
</el-checkbox>
|
||||
</el-row>
|
||||
|
||||
<el-row class="top-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraAll />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
通道总数:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOnline />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
在线:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOnlineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOffline />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
离线:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOfflineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="bottom-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventAll />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
事件总数:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ eventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventClosed />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
已处理:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ closedEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventPending />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
未处理:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ pendintingEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="channel-row">
|
||||
<Channel />
|
||||
</el-row>
|
||||
|
||||
<!-- el-dialog 用于显示通知详情 -->
|
||||
<!-- <el-dialog
|
||||
title="告警详情"
|
||||
v-model="dialogVisible"
|
||||
width="50%"
|
||||
@close="handleDialogClose">
|
||||
<el-row :gutter="30" style="margin-bottom: 2vh;">
|
||||
<el-col span="8">告警编号:{{ dialogContent.id }}</el-col>
|
||||
<el-col span="24">摄像头编号:{{ dialogContent.camera_id }}</el-col>
|
||||
<el-col span="8">摄像头名称:{{ dialogContent.camera?.name }}</el-col>
|
||||
<el-col span="8">告警类型:{{ algorithmMap.get(dialogContent.types) || '未知类型' }}</el-col>
|
||||
<el-col span="8">告警时间:{{ dialogContent.started_at }}</el-col>
|
||||
</el-row>
|
||||
<img :src="dialogContent.snapshotUrl" alt="告警图片" v-if="dialogContent.snapshotUrl" style="max-width: 100%;" />
|
||||
<video v-if="dialogContent.videoUrl" :src="dialogContent.videoUrl" controls style="max-width: 100%;"></video>
|
||||
</el-dialog> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, inject, onMounted } from 'vue';
|
||||
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||
import type { GlobalWebSocket } from '../utils/useGlobalWebSocket';
|
||||
import Channel from '@/components/Channel.vue';
|
||||
import { setDialogHandler } from '../utils/useGlobalWebSocket';
|
||||
import { BoxApi } from '@/utils/boxApi';
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import CameraAll from '@/icons/CameraAll.vue';
|
||||
import CameraOnline from '@/icons/CameraOnline.vue';
|
||||
import CameraOffline from '@/icons/CameraOffline.vue';
|
||||
import EventAll from '@/icons/EventAll.vue';
|
||||
import EventClosed from '@/icons/EventClosed.vue';
|
||||
import EventPending from '@/icons/EventPending.vue';
|
||||
// const apiInstance = new BoxApi();
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const cameraCount = ref(0);
|
||||
const cameraOfflineCount = ref(0);
|
||||
const cameraOnlineCount = ref(0);
|
||||
const eventCount = ref(0);
|
||||
const pendintingEventCount = ref(0);
|
||||
const closedEventCount = ref(0);
|
||||
const isPopupEnabled = ref(false);
|
||||
const globalWebSocket = inject<GlobalWebSocket>('globalWebSocket');
|
||||
|
||||
const activeTab = ref('all');
|
||||
|
||||
const getTodayData = () => {
|
||||
const today = new Date();
|
||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfToday),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const getWeekData = () => {
|
||||
const today = new Date();
|
||||
const startOfWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfWeek),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const getMonthData = () => {
|
||||
const today = new Date();
|
||||
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfMonth),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
cameraCount.value = firstResponse.count;
|
||||
console.log("总数》》》》》》》》》》》》》", cameraCount.value)
|
||||
allCameras = firstResponse.results;
|
||||
// const dialogVisible = ref(false);
|
||||
// const dialogContent = ref({
|
||||
// id: null,
|
||||
// camera_id: null,
|
||||
// camera: { name: '' },
|
||||
// types: null,
|
||||
// started_at: null,
|
||||
// snapshotUrl: '',
|
||||
// videoUrl: ''
|
||||
// });
|
||||
// const algorithmMap = ref(new Map());
|
||||
|
||||
|
||||
const total = cameraCount.value;
|
||||
while (offset + limit < total) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
if (!globalWebSocket) {
|
||||
throw new Error('globalWebSocket 注入失败');
|
||||
}
|
||||
|
||||
allCameras.forEach((camera) => {
|
||||
if (camera.status === 'online') {
|
||||
cameraOnlineCount.value++;
|
||||
} else if (camera.status === 'offline') {
|
||||
cameraOfflineCount.value++;
|
||||
} else {
|
||||
exceptionCount.value++;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching cameras:', error);
|
||||
// 在页面加载时,从 localStorage 中读取勾选状态并注册 WebSocket 回调
|
||||
onMounted(async () => {
|
||||
const storedState = localStorage.getItem('isPopupEnabled');
|
||||
isPopupEnabled.value = storedState === 'true';
|
||||
if (isPopupEnabled.value && !globalWebSocket.isWebSocketConnected.value) {
|
||||
globalWebSocket.connectWebSocket();
|
||||
}
|
||||
|
||||
// try {
|
||||
// const token = localStorage.getItem('alertToken');
|
||||
// const algorithms = await apiInstance.getAlgorithms(token);
|
||||
|
||||
|
||||
// algorithmMap.value = new Map(algorithms.map((algo: { code_name: string, name: string }) => [algo.code_name, algo.name]));
|
||||
|
||||
|
||||
// setDialogHandler(showDialog);
|
||||
|
||||
|
||||
// if (isPopupEnabled.value && !globalWebSocket.isWebSocketConnected.value) {
|
||||
// globalWebSocket.connectWebSocket();
|
||||
// }
|
||||
// } catch (error) {
|
||||
// ElMessage.error('获取算法映射失败');
|
||||
// console.error(error);
|
||||
// }
|
||||
});
|
||||
|
||||
// 当用户勾选或取消勾选时触发的函数
|
||||
const handleCheckboxChange = () => {
|
||||
if (isPopupEnabled.value) {
|
||||
ElMessageBox.confirm('是否开启弹窗提示?', '提示', {
|
||||
confirmButtonText: '是',
|
||||
cancelButtonText: '否',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
localStorage.setItem('isPopupEnabled', 'true');
|
||||
globalWebSocket.connectWebSocket();
|
||||
})
|
||||
.catch(() => {
|
||||
isPopupEnabled.value = false;
|
||||
localStorage.setItem('isPopupEnabled', 'false');
|
||||
});
|
||||
} else {
|
||||
globalWebSocket.closeWebSocket();
|
||||
localStorage.setItem('isPopupEnabled', 'false');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// const fetchEvents = async (timeAfter = null, timeBefore = null) => {
|
||||
// const showDialog = async (data: any) => {
|
||||
// try {
|
||||
// const token = localStorage.getItem('alertToken');
|
||||
// const token = localStorage.getItem('alertToken');
|
||||
// const eventDetails = await apiInstance.getEventById(data.id, token);
|
||||
|
||||
// const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter);
|
||||
// eventCount.value = totalResponse.count;
|
||||
|
||||
// const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
|
||||
// pendintingEventCount.value = pendingResponse.count;
|
||||
|
||||
// const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'closed');
|
||||
// closedEventCount.value = closedResponse.count;
|
||||
// const snapshot = eventDetails.mediums.find((item: any) => item.name === 'snapshot');
|
||||
// const video = eventDetails.mediums.find((item: any) => item.name === 'video');
|
||||
|
||||
// dialogContent.value = {
|
||||
// id: eventDetails.id,
|
||||
// camera_id: eventDetails.camera_id,
|
||||
// camera: eventDetails.camera,
|
||||
// types: eventDetails.types,
|
||||
// started_at: eventDetails.started_at,
|
||||
// snapshotUrl: snapshot?.file || '',
|
||||
// videoUrl: video?.file || ''
|
||||
// };
|
||||
|
||||
// dialogVisible.value = true;
|
||||
// } catch (error) {
|
||||
// console.error('获取事件数据失败:', error);
|
||||
// ElMessage.error('获取告警详情失败');
|
||||
// console.error(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
let timeRange = { timeAfter: null, timeBefore: null };
|
||||
|
||||
// 根据 activeTab 设置时间范围
|
||||
if (activeTab.value === 'today') {
|
||||
timeRange = getTodayData();
|
||||
} else if (activeTab.value === 'week') {
|
||||
timeRange = getWeekData();
|
||||
} else if (activeTab.value === 'month') {
|
||||
timeRange = getMonthData();
|
||||
}
|
||||
|
||||
// 获取告警总数
|
||||
const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter);
|
||||
eventCount.value = totalResponse.count;
|
||||
|
||||
// 获取状态为 pending 的告警数量
|
||||
const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'pending');
|
||||
pendintingEventCount.value = pendingResponse.count;
|
||||
|
||||
// 获取状态为 closed 的告警数量
|
||||
const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'closed');
|
||||
closedEventCount.value = closedResponse.count;
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取事件数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(activeTab, (newTab) => {
|
||||
fetchEvents();
|
||||
});
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// getTodayData();
|
||||
// getWeekData();
|
||||
// getMonthData();
|
||||
fetchCameras();
|
||||
fetchEvents();
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
// const handleDialogClose = () => {
|
||||
// dialogContent.value = {
|
||||
// id: null,
|
||||
// camera_id: null,
|
||||
// camera: { name: '' },
|
||||
// types: null,
|
||||
// started_at: null,
|
||||
// snapshotUrl: '',
|
||||
// videoUrl: ''
|
||||
// };
|
||||
// dialogVisible.value = false;
|
||||
// };
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.top-row,
|
||||
.bottom-row {
|
||||
.settings-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #ffffff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
margin-bottom: 20px;
|
||||
height: 20vh;
|
||||
width: 80vw;
|
||||
padding: 1vh 1vw;
|
||||
margin: 1vh 2vw;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.channel-row {
|
||||
margin: 1vh 2vw;
|
||||
width: 80vw;
|
||||
height: 70vh;
|
||||
background-color: #001529;
|
||||
color: aliceblue;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.inner-count-text {
|
||||
color: rgb(91, 224, 241);
|
||||
}
|
||||
|
||||
.tab-div{
|
||||
background-color: #001529;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
margin-left: 1vh;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: #2ea0ec;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
/* width: 15vw; */
|
||||
position: static !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,7 @@ import '@/assets/global.css'
|
||||
import { useGlobalWebSocket } from './utils/useGlobalWebSocket';
|
||||
import DataVVue3 from '@kjgl77/datav-vue3';
|
||||
import '@kjgl77/datav-vue3/dist/style.css';
|
||||
import mitt from 'mitt';
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
@@ -74,4 +75,7 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateDimensions);
|
||||
});
|
||||
|
||||
const eventBus = mitt();
|
||||
app.config.globalProperties.$eventBus = eventBus;
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -16,6 +16,7 @@ class BoxApi {
|
||||
private readonly apiResetPassword: string = "/auth/reset_password";
|
||||
private readonly getMinCamerasApi: string = "/camera/cameras/get_all";
|
||||
private readonly getMinCamera: string = "/camera/cameras";
|
||||
private readonly getEventByIdUrl: string = "/event/events/retrieves";
|
||||
|
||||
private readonly loginConfig: object = {
|
||||
headers: {
|
||||
@@ -313,6 +314,7 @@ class BoxApi {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async getEventsByParams(
|
||||
token: string | null = null,
|
||||
pageSize: number = 20,
|
||||
@@ -366,6 +368,26 @@ class BoxApi {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getEventById(id: number, token: string | null = null): Promise<any> {
|
||||
try {
|
||||
const url = `${this.getEventByIdUrl}`;
|
||||
const params = { id };
|
||||
const res = await this.axios.get(url, {
|
||||
...this._authHeader(token),
|
||||
params: params
|
||||
});
|
||||
|
||||
if (res.data.err.ec === 0) {
|
||||
// return res.data.ret.objects[0];
|
||||
return res.data.ret.objects[0];
|
||||
} else {
|
||||
throw new Error(res.data.err.dm);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// public async getOneEvent(token: string | null = null): Promise<any> {
|
||||
// try {
|
||||
// return await this.getEvents(1, 0, token);
|
||||
|
||||
5
src/utils/eventBus.ts
Normal file
5
src/utils/eventBus.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
import mitt from 'mitt';
|
||||
// 创建事件总线实例
|
||||
const eventBus = mitt();
|
||||
export default eventBus;
|
||||
@@ -1,22 +1,23 @@
|
||||
// useGlobalWebSocket.ts
|
||||
import { ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElNotification } from 'element-plus';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
|
||||
const websocket = ref<WebSocket | null>(null);
|
||||
const isWebSocketConnected = ref(false);
|
||||
let heartbeatInterval: number | null = null;
|
||||
const rememberedAddress = localStorage.getItem('rememberedAddress') || '127.0.0.1';
|
||||
|
||||
// 连接 WebSocket
|
||||
const connectWebSocket = () => {
|
||||
websocket.value = new WebSocket(`ws://${rememberedAddress}:8080/ws/event`);
|
||||
websocket.value = new WebSocket(`ws://192.168.28.33:8080/ws/event`);
|
||||
websocket.value.onopen = () => {
|
||||
ElMessage.success('全局 WebSocket 连接成功');
|
||||
isWebSocketConnected.value = true;
|
||||
startHeartbeat();
|
||||
};
|
||||
websocket.value.onmessage = (event) => {
|
||||
showNotification(event.data);
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// 显示通知,并在点击时通过 eventBus 触发全局弹窗事件
|
||||
showNotification(data);
|
||||
};
|
||||
websocket.value.onclose = handleClose;
|
||||
websocket.value.onerror = handleError;
|
||||
@@ -32,7 +33,7 @@ const closeWebSocket = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 心跳检测
|
||||
// 启动心跳
|
||||
const startHeartbeat = () => {
|
||||
if (heartbeatInterval) return;
|
||||
heartbeatInterval = window.setInterval(() => {
|
||||
@@ -42,6 +43,7 @@ const startHeartbeat = () => {
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// 停止心跳
|
||||
const stopHeartbeat = () => {
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval);
|
||||
@@ -54,32 +56,34 @@ const handleClose = () => {
|
||||
ElMessage.warning('WebSocket 连接已关闭');
|
||||
isWebSocketConnected.value = false;
|
||||
stopHeartbeat();
|
||||
localStorage.setItem('isPopupEnabled', 'False');
|
||||
};
|
||||
|
||||
// 处理错误
|
||||
const handleError = () => {
|
||||
ElMessage.error('WebSocket 连接出错');
|
||||
isWebSocketConnected.value = false;
|
||||
stopHeartbeat();
|
||||
};
|
||||
|
||||
// 显示通知
|
||||
const showNotification = (message: string) => {
|
||||
if (Notification.permission === 'granted') {
|
||||
new Notification('新消息', {
|
||||
body: message,
|
||||
icon: '/path/to/icon.png',
|
||||
});
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
Notification.requestPermission().then((permission) => {
|
||||
if (permission === 'granted') {
|
||||
new Notification('新消息', {
|
||||
body: message,
|
||||
icon: '/path/to/icon.png',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// 显示自定义通知
|
||||
const showNotification = (data: any) => {
|
||||
ElNotification({
|
||||
title: '新告警',
|
||||
message: `
|
||||
<div style="max-height: 200px; overflow-y: auto;">
|
||||
<p><strong>告警编号:</strong>${data.id || '未知'}</p>
|
||||
<p><strong>摄像头编号:</strong>${data.camera_id || '未知'}</p>
|
||||
<p><strong>摄像头名称:</strong>${data.camera.name || '未知'}</p>
|
||||
</div>
|
||||
`,
|
||||
dangerouslyUseHTMLString: true,
|
||||
duration: 5000,
|
||||
customClass: 'custom-notification',
|
||||
onClick: () => {
|
||||
// 触发全局事件总线的 showDialog 事件
|
||||
eventBus.emit('showDialog', data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 导出类型和方法
|
||||
|
||||
@@ -7,5 +7,13 @@
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user