DataStatistics数据显示页面---test.vue中测试全局弹窗提示

This commit is contained in:
龚皓
2024-11-14 16:07:42 +08:00
parent a2234922c8
commit fd75d299ec
24 changed files with 3637 additions and 1582 deletions

59
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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>

View File

View 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>

View 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>

View 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>

View 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>

View File

@@ -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
View 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> &nbsp;名称: {{ 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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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');

View File

@@ -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); // 重新统计数量,添加时间条件
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 >
&nbsp;告警数据面板&nbsp;
</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>

View File

@@ -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>

View File

@@ -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')

View File

@@ -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
View File

@@ -0,0 +1,5 @@
import mitt from 'mitt';
// 创建事件总线实例
const eventBus = mitt();
export default eventBus;

View File

@@ -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);
}
});
};
// 导出类型和方法

View File

@@ -7,5 +7,13 @@
{
"path": "./tsconfig.app.json"
}
]
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}