DataStatistics数据显示页面---test.vue中测试全局弹窗提示
This commit is contained in:
@@ -8,40 +8,52 @@
|
||||
<alertChart />
|
||||
</el-col>
|
||||
</el-row> -->
|
||||
<el-row class="middle-row">
|
||||
<!-- <el-row class="middle-row">
|
||||
<span>
|
||||
告警总数:{{ displayTotalItems }}
|
||||
</span>
|
||||
</el-row>
|
||||
</el-row> -->
|
||||
<el-row class="filter-row">
|
||||
<el-col :span="5">
|
||||
<el-col :span="4">
|
||||
<el-form-item label="摄像头名称">
|
||||
<el-select v-model="filterParams.cameraId" placeholder="请选择摄像头" filterable>
|
||||
<el-select v-model="filterParams.cameraId" placeholder="请选择摄像头" filterable clearable>
|
||||
<el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-form-item label="告警类型">
|
||||
<el-select v-model="filterParams.types" placeholder="请选择告警类型">
|
||||
<el-select v-model="filterParams.types" placeholder="请选择类型" clearable>
|
||||
<el-option v-for="(label, key) in typeMapping" :key="key" :label="label" :value="key"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<el-col :span="3">
|
||||
<el-form-item label="处理情况">
|
||||
<el-select v-model="filterParams.status" placeholder="全部" clearable>
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="待处理" value="pending"></el-option>
|
||||
<el-option label="已处理" value="closed"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="开始时间">
|
||||
<el-date-picker v-model="filterParams.timeAfter" type="datetime" placeholder="请选择开始时间"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<el-col :span="4">
|
||||
<el-form-item label="结束时间">
|
||||
<el-date-picker v-model="filterParams.timeBefore" type="datetime" placeholder="请选择结束时间"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="2" class="filter-buttons">
|
||||
<el-col :span="4" class="filter-buttons">
|
||||
<el-button type="primary" @click="handleFilter">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button :disabled="isExporting" @click="exportData">
|
||||
{{ isExporting ? '正在导出,请勿重复点击' : '导出' }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
@@ -181,6 +193,9 @@ import { ref, reactive, onMounted, computed } from 'vue';
|
||||
// import AlertChart from '@/components/AlertChart.vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
// 创建 BoxApi 实例
|
||||
const boxApi = new BoxApi();
|
||||
|
||||
@@ -208,6 +223,66 @@ const totalItems = ref(0);
|
||||
const displayTotalItems = ref(0); // 用于展示的数字
|
||||
const cameras = ref([]);
|
||||
|
||||
const formatDateTimeToISO = (datetime) => {
|
||||
return new Date(datetime).toISOString().replace('.000', '');
|
||||
};
|
||||
|
||||
|
||||
const isExporting = ref(false);
|
||||
// 导出数据方法
|
||||
const exportData = async () => {
|
||||
if (isExporting.value) return; // 如果已经在导出,直接返回,避免重复点击
|
||||
|
||||
isExporting.value = true; // 设置为正在导出状态
|
||||
try {
|
||||
const allData = [];
|
||||
let page = 1;
|
||||
let totalPages = 1;
|
||||
const limit = 500;
|
||||
|
||||
// 循环分页请求所有数据
|
||||
do {
|
||||
const { results, count } = await boxApi.getEventsByParams(
|
||||
token.value,
|
||||
limit,
|
||||
page,
|
||||
filterParams.timeBefore ? formatDateTimeToISO(filterParams.timeBefore) : null,
|
||||
filterParams.timeAfter ? formatDateTimeToISO(filterParams.timeAfter) : null,
|
||||
filterParams.types,
|
||||
filterParams.cameraId,
|
||||
filterParams.status
|
||||
);
|
||||
|
||||
// 格式化数据
|
||||
const formattedData = results.map(item => ({
|
||||
'告警编号': item.id,
|
||||
'告警类型': typeMapping[item.types] || item.types,
|
||||
'告警位置': item.camera ? item.camera.name : '',
|
||||
'告警时间': formatDateTime(item.ended_at),
|
||||
'告警状态': statusMapping[item.status] || item.status
|
||||
}));
|
||||
|
||||
allData.push(...formattedData);
|
||||
totalPages = Math.ceil(count / limit);
|
||||
page += 1;
|
||||
} while (page <= totalPages);
|
||||
|
||||
// 使用 PapaParse 将 JSON 转换为 CSV,指定列标题
|
||||
const csv = Papa.unparse(allData, {
|
||||
columns: ['告警编号', '告警类型', '告警位置', '告警时间', '告警状态']
|
||||
});
|
||||
|
||||
// 添加 BOM,确保中文编码正确
|
||||
const blob = new Blob(["\uFEFF" + csv], { type: 'text/csv;charset=utf-8;' });
|
||||
saveAs(blob, 'Alert_data.csv');
|
||||
} catch (error) {
|
||||
console.error("导出数据错误:", error);
|
||||
}finally {
|
||||
isExporting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
@@ -240,6 +315,8 @@ const filterParams = reactive({
|
||||
types: null,
|
||||
timeAfter: null,
|
||||
timeBefore: null,
|
||||
cameraId: null,
|
||||
status: null,
|
||||
});
|
||||
|
||||
|
||||
@@ -273,9 +350,10 @@ const handleFilter = async () => {
|
||||
const types = filterParams.types || null;
|
||||
// const timeAfter = filterParams.timeAfter ? new Date(filterParams.timeAfter).toISOString() : null;
|
||||
// const timeBefore = filterParams.timeBefore ? new Date(filterParams.timeBefore).toISOString() : null;
|
||||
const timeAfter = filterParams.timeAfter ? formatDateTime(new Date(filterParams.timeAfter)) : null;
|
||||
const timeBefore = filterParams.timeBefore ? formatDateTime(new Date(filterParams.timeBefore)) : null;
|
||||
const timeAfter = filterParams.timeAfter ? formatDateTimeToISO(new Date(filterParams.timeAfter)) : null;
|
||||
const timeBefore = filterParams.timeBefore ? formatDateTimeToISO(new Date(filterParams.timeBefore)) : null;
|
||||
const cameraId = filterParams.cameraId || null;
|
||||
const status = filterParams.status || null;
|
||||
|
||||
const { results, count } = await boxApi.getEventsByParams(
|
||||
token.value,
|
||||
@@ -284,7 +362,8 @@ const handleFilter = async () => {
|
||||
timeBefore,
|
||||
timeAfter,
|
||||
types,
|
||||
cameraId
|
||||
cameraId,
|
||||
status
|
||||
);
|
||||
tableData.value = results;
|
||||
totalItems.value = count;
|
||||
@@ -298,6 +377,7 @@ const handleReset = () => {
|
||||
filterParams.timeAfter = null;
|
||||
filterParams.timeBefore = null;
|
||||
filterParams.cameraId = null;
|
||||
filterParams.status = null;
|
||||
fetchEvents(); // 重置筛选条件后,重新获取所有告警数据
|
||||
};
|
||||
|
||||
@@ -386,7 +466,7 @@ const closeDialog = () => {
|
||||
// 分页处理
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page;
|
||||
if (filterParams.types || filterParams.timeAfter || filterParams.timeBefore) {
|
||||
if (filterParams.types || filterParams.timeAfter || filterParams.timeBefore || filterParams.cameraId || filterParams.status) {
|
||||
handleFilter();
|
||||
} else {
|
||||
fetchEvents();
|
||||
@@ -434,7 +514,9 @@ onMounted(async () => {
|
||||
.alert-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f5f7fa;
|
||||
background-color: #001529;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* .top-pan {
|
||||
@@ -453,7 +535,7 @@ onMounted(async () => {
|
||||
border-radius: 15px;
|
||||
} */
|
||||
|
||||
.middle-row {
|
||||
/* .middle-row {
|
||||
min-height: 5vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -461,14 +543,43 @@ onMounted(async () => {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(to bottom left, rgb(150, 151, 243), rgb(215, 214, 250));
|
||||
/* border-radius: 8px; */
|
||||
border-radius: 8px;
|
||||
} */
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
height: 15vh;
|
||||
padding: 0px 0;
|
||||
margin: 5vh 6vw 5vh 1vw;
|
||||
gap: 8px;
|
||||
/* font-weight: bold; */
|
||||
font-size: 15px;
|
||||
background-color: rgb(251, 254, 255);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
max-width: 100%;
|
||||
height: 80%;
|
||||
/* min-height: 50vh; */
|
||||
/* max-height: 70vh; */
|
||||
overflow-x: auto;
|
||||
margin: 0vh 6vw 0vh 1vw;
|
||||
padding: 0;
|
||||
overflow-y: hidden;
|
||||
border-radius: 8px 8px 0 0;
|
||||
background-color: white;
|
||||
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
background-color: #e9eefc;
|
||||
padding-left: 20px;
|
||||
margin: 0vh 6vw 5vh 1vw;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
@@ -477,8 +588,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
::v-deep .el-table th.el-table__cell {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
background-color: #e9eefca4;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.event-media {
|
||||
@@ -522,12 +633,7 @@ onMounted(async () => {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
background-color: #e8e9e4;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
|
||||
::v-deep .pagination-container .el-pagination__total,
|
||||
::v-deep .pagination-container .el-pagination__goto,
|
||||
@@ -541,15 +647,4 @@ onMounted(async () => {
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
height: 80px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
gap: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,124 +1,702 @@
|
||||
<template>
|
||||
<div class="alert-container" >
|
||||
<el-row class="bottom-pan">
|
||||
<el-col class="panel-bottom">
|
||||
<Cameras/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="static-container">
|
||||
|
||||
<el-row class="top-pan">
|
||||
<el-col :sm="24" :md="12" class="panel-top-left">
|
||||
<statistics />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" class="panel-top-right">
|
||||
<alertChart />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="search-row">
|
||||
<el-date-picker v-model="filterParams.timeAfter" :teleported="false" type="datetime" placeholder="请选择开始时间"
|
||||
prefix-icon="CaretBottom" popper-class="popperClass"></el-date-picker>
|
||||
<el-date-picker v-model="filterParams.timeBefore" :teleported="false" type="datetime" placeholder="请选择结束时间"
|
||||
prefix-icon="CaretBottom" popper-class="popperClass"></el-date-picker>
|
||||
<el-button type="primary" @click="handleFilter" class="alert-bt">查询</el-button>
|
||||
<el-button type="primary" @click="handleReset" class="alert-bt">清空</el-button>
|
||||
</div>
|
||||
|
||||
<div class="second-row">
|
||||
<el-row class="top-part">
|
||||
<el-col :sm="24" :md="6">
|
||||
<div class="camera-stats">
|
||||
<div id="camera-chart-container" class="camera-status-chart"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="6">
|
||||
<div class="event-stats">
|
||||
<div id="event-chart-container" class="event-status-chart"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12">
|
||||
<div class="part-three">
|
||||
<AlertChart />
|
||||
</div>
|
||||
</el-col>
|
||||
<!-- <el-col :sm="24" :md="6" >
|
||||
<div class="part-four">
|
||||
|
||||
</div>
|
||||
</el-col> -->
|
||||
</el-row>
|
||||
|
||||
<el-row class="bottom-part">
|
||||
<el-col :span="24">
|
||||
<div class="bottom-row">
|
||||
<div id="code-chart-container" class="code-status-chart"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- <el-row>
|
||||
<el-col :span="24">
|
||||
<div class="statistics-container">
|
||||
<p v-for="(name, code) in typeMapping" :key="code">
|
||||
{{ name }}: {{ typeCounts[code] || 0 }} 次
|
||||
</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row> -->
|
||||
|
||||
<!--<p v-for="(name, code) in typeMapping" :key="code">
|
||||
{{ name }}: 总数 {{ typeCounts[code]?.total || 0 }} 次, 处理中 {{ typeCounts[code]?.pending || 0 }} 次, 已处理 {{
|
||||
typeCounts[code]?.closed || 0 }} 次
|
||||
</p> -->
|
||||
<!-- 摄像总数{{ cameraCount }}
|
||||
摄像在线{{ cameraOnlineCount }}
|
||||
摄像离线{{ cameraOfflineCount }}
|
||||
事件总数{{ eventCount }}
|
||||
处理中{{ pendingEventCount }}
|
||||
已处理{{ closedEventCount }} -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted,computed, onBeforeUnmount} from 'vue';
|
||||
import Statistics from '@/components/Statistics.vue';
|
||||
import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import * as echarts from 'echarts';
|
||||
import AlertChart from '@/components/AlertChart.vue';
|
||||
import Cameras from '@/components/Cameras.vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
||||
|
||||
// 创建 BoxApi 实例
|
||||
const boxApi = new BoxApi();
|
||||
const apiInstance = new BoxApi();
|
||||
const typeMapping = reactive({});
|
||||
const token = ref(null);
|
||||
// const scale = ref(1);
|
||||
const typeCounts = reactive({});
|
||||
const filterParams = reactive({
|
||||
timeAfter: null,
|
||||
timeBefore: null,
|
||||
});
|
||||
|
||||
// const scaleStyle = computed(() => ({
|
||||
// transform: `scale(${scale.value})`,
|
||||
// transformOrigin: 'top left',
|
||||
// width: `${100 / scale.value}%`,
|
||||
// }));
|
||||
const xData = ref([]);
|
||||
const totalData = ref([]);
|
||||
const pendingData = ref([]);
|
||||
|
||||
// const handleResize = () => {
|
||||
// const clientWidth = document.documentElement.clientWidth;
|
||||
// const scaleFactor = clientWidth / 1920;
|
||||
// scale.value = scaleFactor < 1 ? scaleFactor : 1;
|
||||
// };
|
||||
const cameraCount = ref(0);
|
||||
const cameraOfflineCount = ref(0);
|
||||
const cameraOnlineCount = ref(0);
|
||||
|
||||
// 获取类型映射
|
||||
const eventCount = ref(0);
|
||||
const pendingEventCount = ref(0);
|
||||
const closedEventCount = ref(0);
|
||||
|
||||
const formatDateTimeToISO = (datetime) => {
|
||||
return new Date(datetime).toISOString().replace('.000', '');
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
// filterParams.types = null;
|
||||
filterParams.timeAfter = null;
|
||||
filterParams.timeBefore = null;
|
||||
// filterParams.cameraId = null;
|
||||
// filterParams.status = null;
|
||||
handleFilter();
|
||||
};
|
||||
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
|
||||
const fetchAllCounts = async (timeAfter, timeBefore) => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter);
|
||||
eventCount.value = totalResponse.count;
|
||||
|
||||
// const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
|
||||
// pendingEventCount.value = pendingResponse.count;
|
||||
const { count: pendingCount } = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
|
||||
pendingEventCount.value = pendingCount;
|
||||
|
||||
|
||||
const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'closed');
|
||||
closedEventCount.value = closedResponse.count;
|
||||
} catch (error) {
|
||||
console.error('Error fetching all counts:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
cameraCount.value = firstResponse.count;
|
||||
// console.log("总数》》》》》》》》》》》》》", cameraCount.value)
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
|
||||
const total = cameraCount.value;
|
||||
while (offset + limit < total) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
|
||||
allCameras.forEach((camera) => {
|
||||
if (camera.status === 'online') {
|
||||
cameraOnlineCount.value++;
|
||||
} else if (camera.status === 'offline') {
|
||||
cameraOfflineCount.value++;
|
||||
} else {
|
||||
exceptionCount.value++;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching cameras:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 获取告警类型并初始化每种类型的count
|
||||
const fetchTypeMapping = async (token) => {
|
||||
try {
|
||||
const algorithms = await boxApi.getAlgorithms(token); // 使用 BoxApi 的 getAlgorithms 方法
|
||||
const algorithms = await apiInstance.getAlgorithms(token);
|
||||
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
||||
|
||||
algorithms.forEach((algorithm) => {
|
||||
typeMapping[algorithm.code_name] = algorithm.name;
|
||||
typeCounts[algorithm.code_name] = { total: 0, pending: 0, closed: 0 };
|
||||
});
|
||||
|
||||
additionalMappings.forEach((item) => {
|
||||
typeMapping[item.code_name] = item.name;
|
||||
typeCounts[item.code_name] = { total: 0, pending: 0, closed: 0 };
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching algorithms:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTypeCounts = async (timeAfter, timeBefore) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const tempCounts = []; // 临时数组用于存储类型和总数
|
||||
|
||||
// 清空数据以确保每次查询更新时数据正确
|
||||
xData.value = [];
|
||||
totalData.value = [];
|
||||
pendingData.value = [];
|
||||
|
||||
// 遍历每种类型,根据 code_name 获取总数
|
||||
for (const code in typeMapping) {
|
||||
try {
|
||||
const { count: totalCount } = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, code);
|
||||
tempCounts.push({ code, name: typeMapping[code], total: totalCount });
|
||||
} catch (error) {
|
||||
console.error(`Error fetching total count for type ${code}:`, error);
|
||||
tempCounts.push({ code, name: typeMapping[code], total: 0 }); // 如果有错误,默认总数为 0
|
||||
}
|
||||
}
|
||||
|
||||
// 根据总数降序排序
|
||||
tempCounts.sort((a, b) => b.total - a.total);
|
||||
|
||||
// 更新 xData、totalData 和 pendingData 按排序后的顺序
|
||||
xData.value = tempCounts.map(item => item.name); // 排序后的名称顺序
|
||||
totalData.value = tempCounts.map(item => item.total); // 排序后的总数顺序
|
||||
|
||||
// 获取每种类型的处理中数量,并按排序顺序赋值到 pendingData
|
||||
for (const item of tempCounts) {
|
||||
try {
|
||||
const { count: pendingCount } = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, item.code, null, 'pending');
|
||||
pendingData.value.push(pendingCount);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching pending count for type ${item.code}:`, error);
|
||||
pendingData.value.push(0); // 如果有错误,默认处理中数量为 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilter = async () => {
|
||||
const timeAfter = filterParams.timeAfter ? formatDateTimeToISO(filterParams.timeAfter) : null;
|
||||
const timeBefore = filterParams.timeBefore ? formatDateTimeToISO(filterParams.timeBefore) : null;
|
||||
|
||||
console.log('timeAfter:', timeAfter);
|
||||
await fetchTypeCounts(timeAfter, timeBefore);
|
||||
await fetchAllCounts(timeAfter, timeBefore);
|
||||
|
||||
|
||||
renderChart();
|
||||
renderCameraChart();
|
||||
renderEventChart();
|
||||
};
|
||||
|
||||
|
||||
const renderCameraChart = () => {
|
||||
const chartDom = document.getElementById('camera-chart-container');
|
||||
const chart = echarts.init(chartDom);
|
||||
|
||||
const chartData = { ONE: cameraOnlineCount.value, TWO: cameraOfflineCount.value, num: cameraCount.value };
|
||||
const seriesData = [];
|
||||
const legendData = [];
|
||||
const nameList = [
|
||||
{ key: 'ONE', text: '在线', color: '#243eff' },
|
||||
{ key: 'TWO', text: '离线', color: '#2490ff' },
|
||||
// { key: 'num', text: '总数' },
|
||||
];
|
||||
|
||||
nameList.forEach((item) => {
|
||||
seriesData.push({
|
||||
value: chartData[item.key],
|
||||
name: item.text,
|
||||
itemStyle: { color: item.color },
|
||||
});
|
||||
legendData.push(item.text); // 直接添加到 legendData 数组中
|
||||
});
|
||||
|
||||
const option = {
|
||||
backgroundColor: '#001529',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: ({ name, value, percent }) => {
|
||||
const color = nameList.find((item) => item.text === name).color;
|
||||
return `<span style="color:${color};">●</span> ${name} 数量: ${value}<br><span style="color:${color};">●</span> ${name} 比例: ${percent}%`;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical', // 设置纵向排列
|
||||
top: '8%',
|
||||
left: '10%',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
},
|
||||
icon: 'circle',
|
||||
data: legendData, // 使用 legendData 数组作为数据
|
||||
formatter: (name) => {
|
||||
const item = nameList.find((n) => n.text === name);
|
||||
return `${name} ${chartData[item.key] || 0}`;
|
||||
},
|
||||
rich: nameList.reduce((acc, item) => {
|
||||
acc[item.key] = {
|
||||
color: item.color,
|
||||
fontSize: 14,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
color: ['#243eff', '#2490ff', '#30f3e3'],
|
||||
series: [
|
||||
{
|
||||
name: '摄像头状态',
|
||||
type: 'pie',
|
||||
center: ['50%', '60%'],
|
||||
radius: ['40%', '50%'],
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 15,
|
||||
formatter: () => `摄像状态`,
|
||||
},
|
||||
labelLine: { show: false },
|
||||
data: seriesData,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
};
|
||||
|
||||
const renderEventChart = () => {
|
||||
const chartDom = document.getElementById('event-chart-container');
|
||||
const chart = echarts.init(chartDom);
|
||||
|
||||
const chartData = { ONE: pendingEventCount.value, TWO: closedEventCount.value, num: eventCount.value };
|
||||
const seriesData = [];
|
||||
const legendData = [];
|
||||
const nameList = [
|
||||
{ key: 'ONE', text: '处理中', color: '#243eff' },
|
||||
{ key: 'TWO', text: '已处理', color: '#2490ff' },
|
||||
];
|
||||
|
||||
nameList.forEach((item) => {
|
||||
seriesData.push({
|
||||
value: chartData[item.key],
|
||||
name: item.text,
|
||||
itemStyle: { color: item.color },
|
||||
});
|
||||
legendData.push(item.text);
|
||||
});
|
||||
|
||||
const option = {
|
||||
backgroundColor: '#001529',
|
||||
color: ['#243eff', '#2490ff', '#30f3e3'],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: ({ name, value, percent }) => {
|
||||
const color = nameList.find((item) => item.text === name).color;
|
||||
return `<span style="color:${color};">●</span> ${name} 数量: ${value}<br><span style="color:${color};">●</span> ${name} 比例: ${percent}%`;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
top: '5%',
|
||||
left: '10%',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 14,
|
||||
},
|
||||
icon: 'circle',
|
||||
data: legendData,
|
||||
formatter: (name) => {
|
||||
const item = nameList.find((n) => n.text === name);
|
||||
return `${name} ${chartData[item.key] || 0}`;
|
||||
}
|
||||
},
|
||||
rich: nameList.reduce((acc, item) => {
|
||||
acc[item.key] = {
|
||||
color: item.color,
|
||||
fontSize: 14,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
series: [
|
||||
{
|
||||
name: '',
|
||||
type: 'pie',
|
||||
center: ['50%', '60%'],
|
||||
radius: ['40%', '50%'],
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 15,
|
||||
formatter: () => `事件概览 `,
|
||||
},
|
||||
labelLine: { show: false },
|
||||
data: seriesData,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const renderChart = () => {
|
||||
const chartDom = document.getElementById('code-chart-container');
|
||||
const chart = echarts.init(chartDom);
|
||||
|
||||
const option = {
|
||||
backgroundColor: '#001529',
|
||||
title: {
|
||||
text: ` 算法摄像头分布`, // 添加标题文本
|
||||
left: '2%', // 设置标题位置为居中
|
||||
top: '10%', // 设置标题距离顶部的位置
|
||||
textStyle: {
|
||||
color: '#5BFCF4', // 标题颜色
|
||||
fontSize: 16, // 标题字体大小
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '10%',
|
||||
bottom: '5%',
|
||||
right: '5%',
|
||||
containLabel: true,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: xData.value, // 使用排序后的 xData
|
||||
axisTick: { alignWithLabel: true },
|
||||
axisLine: { show: false },
|
||||
axisLabel: { textStyle: { color: '#ddd' }, margin: 30 },
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
show: false,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '上部圆 - 总数',
|
||||
type: 'pictorialBar',
|
||||
silent: true,
|
||||
symbolSize: [40, 10],
|
||||
symbolOffset: [0, -6],
|
||||
symbolPosition: 'end',
|
||||
z: 12,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
fontSize: 15,
|
||||
fontWeight: 'bold',
|
||||
color: '#5BFCF4',
|
||||
},
|
||||
color: '#5BFCF4',
|
||||
data: totalData.value,
|
||||
},
|
||||
{
|
||||
name: '底部圆 - 处理中',
|
||||
type: 'pictorialBar',
|
||||
silent: true,
|
||||
symbolSize: [40, 10],
|
||||
symbolOffset: [0, 7],
|
||||
z: 12,
|
||||
color: '#FE9C5A',
|
||||
data: pendingData.value,
|
||||
},
|
||||
{
|
||||
name: '内层波浪',
|
||||
type: 'pictorialBar',
|
||||
silent: true,
|
||||
symbolSize: [50, 15],
|
||||
symbolOffset: [0, 12],
|
||||
z: 10,
|
||||
itemStyle: {
|
||||
color: 'transparent',
|
||||
borderColor: '#5BFCF4',
|
||||
borderWidth: 8,
|
||||
},
|
||||
data: totalData.value,
|
||||
},
|
||||
{
|
||||
name: '外层波浪',
|
||||
type: 'pictorialBar',
|
||||
silent: true,
|
||||
symbolSize: [70, 20],
|
||||
symbolOffset: [0, 18],
|
||||
z: 10,
|
||||
itemStyle: {
|
||||
color: 'transparent',
|
||||
borderColor: 'rgba(91,252,244,0.5)',
|
||||
borderWidth: 5,
|
||||
},
|
||||
data: totalData.value,
|
||||
},
|
||||
{
|
||||
name: '设备数量 - 总数',
|
||||
type: 'bar',
|
||||
barWidth: '40',
|
||||
barGap: '10%',
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 0.7, [
|
||||
{ offset: 0, color: 'rgba(210,210,210,0.3)' },
|
||||
{ offset: 1, color: '#5BFCF4' },
|
||||
]),
|
||||
opacity: 0.8,
|
||||
},
|
||||
data: totalData.value,
|
||||
},
|
||||
{
|
||||
name: '告警数量 - 处理中',
|
||||
type: 'bar',
|
||||
barWidth: 40,
|
||||
z: 12,
|
||||
barGap: '-100%',
|
||||
itemStyle: {
|
||||
opacity: 0.7,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#EB3B5A' },
|
||||
{ offset: 1, color: '#FE9C5A' },
|
||||
]),
|
||||
},
|
||||
data: pendingData.value,
|
||||
},
|
||||
{
|
||||
name: '上部圆 - 总数',
|
||||
type: 'pictorialBar',
|
||||
silent: true,
|
||||
symbolSize: [40, 10],
|
||||
symbolOffset: [0, -6],
|
||||
symbolPosition: 'end',
|
||||
z: 12,
|
||||
color: '#5BFCF4',
|
||||
data: totalData.value,
|
||||
},
|
||||
{
|
||||
name: '底部圆 - 处理中',
|
||||
type: 'pictorialBar',
|
||||
silent: true,
|
||||
symbolSize: [40, 10],
|
||||
symbolOffset: [0, 7],
|
||||
z: 12,
|
||||
color: '#FE9C5A',
|
||||
data: pendingData.value,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
token.value = localStorage.getItem('alertToken');
|
||||
await fetchTypeMapping(token.value);
|
||||
// handleResize();
|
||||
// window.addEventListener('resize', handleResize);
|
||||
const token = localStorage.getItem('alertToken');
|
||||
|
||||
|
||||
await fetchTypeMapping(token);
|
||||
await fetchTypeCounts();
|
||||
await fetchAllCounts();
|
||||
await fetchCameras();
|
||||
|
||||
renderChart();
|
||||
renderCameraChart();
|
||||
renderEventChart();
|
||||
|
||||
window.addEventListener('resize', () => echarts.getInstanceByDom(document.getElementById('chart-container')).resize());
|
||||
});
|
||||
|
||||
// onBeforeUnmount(() => {
|
||||
// window.removeEventListener('resize', handleResize);
|
||||
// });
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', () => echarts.getInstanceByDom(document.getElementById('chart-container')).resize());
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alert-container {
|
||||
/* transform: scale(0.97); */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f5f7fa;
|
||||
overflow-x: hidden;
|
||||
height: 100vh;
|
||||
.static-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #001529;
|
||||
/* background: linear-gradient(to right bottom, rgb(0, 21, 41), rgba(3, 55, 153, 0.3)); */
|
||||
/* background: repeating-radial-gradient(circle, rgb(7, 7, 7), rgba(14, 14, 211, 0.5) 20px, rgba(3, 0, 151, 0.979) 100px); */
|
||||
/* background: repeating-linear-gradient(45deg, rgba(0, 21, 41, 0.9), rgba(12, 203, 209, 0.5) 20px); */
|
||||
/* background:
|
||||
radial-gradient(circle at 0% 0%, rgba(0, 21, 41,1), rgba(12, 203, 209, 0.2)),
|
||||
radial-gradient(circle at 100% 100%, rgba(12, 203, 209, 0.3), rgba(0, 21, 41, 1)),
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.5), transparent); */
|
||||
|
||||
/* box-sizing: border-box; */
|
||||
}
|
||||
|
||||
.top-pan {
|
||||
/* padding: 10px; */
|
||||
/* margin-bottom: 10px; */
|
||||
.search-row {
|
||||
width: 82vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 15vh;
|
||||
/* background-color: #4587dd; */
|
||||
background: repeating-linear-gradient(-45deg, rgba(0, 21, 41), rgba(58, 61, 214, 0.3) 100px);
|
||||
gap: 1vw;
|
||||
padding: 0 2vw;
|
||||
margin: 2vh 0vw 2vh 3vw;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.second-row {
|
||||
/* display: flex; */
|
||||
/* gap: 5px; */
|
||||
/* background-color: #fff; */
|
||||
/* background-color: #1E2E4A; */
|
||||
/* overflow: hidden; */
|
||||
/* height: 55vh; */
|
||||
/* max-height: 450px; */
|
||||
/* padding-left: 1vh; */
|
||||
/* padding-right:1vh ; */
|
||||
/* overflow: hidden; */
|
||||
height: 56vh;
|
||||
margin-top: 60px;
|
||||
/* border: 1px solid #1E2E4A; */
|
||||
|
||||
}
|
||||
.bottom-pan{
|
||||
margin: 0;
|
||||
/* flex-direction: column; */
|
||||
/* align-items: center; */
|
||||
/* gap: 10px; */
|
||||
padding: 0;
|
||||
height: 33vh;
|
||||
box-sizing: border-box;
|
||||
margin-left: 3vw;
|
||||
width: 86vw;
|
||||
height: 70vh;
|
||||
border-radius: 10px;
|
||||
background-color: #001529;
|
||||
|
||||
}
|
||||
|
||||
.panel-top-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.top-part {
|
||||
margin-left: 1vw;
|
||||
height: 30vh;
|
||||
/* border: solid 1px #70ce18; */
|
||||
}
|
||||
|
||||
.panel-top-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* gap: 20px; */
|
||||
}
|
||||
|
||||
.panel-bottom{
|
||||
margin: 0;
|
||||
.camera-status-chart,
|
||||
.event-status-chart {
|
||||
height: 30vh;
|
||||
width: 20vw;
|
||||
padding: 0;
|
||||
margin: 1vh 0vw 0 0vw;
|
||||
padding: 0;
|
||||
border: none;
|
||||
/* border: solid 1px #ce1827; */
|
||||
}
|
||||
|
||||
.part-three,
|
||||
.part-four {
|
||||
height: 30vh;
|
||||
width: 41vw;
|
||||
/* padding: 2px 0; */
|
||||
margin: 1vh 0vw 0 0;
|
||||
/* border: solid 1px #a418ce; */
|
||||
/* margin: 1vh 1vw 0 0; */
|
||||
}
|
||||
|
||||
/* .event-status-chart{
|
||||
height: 30vh;
|
||||
width: 24vw;
|
||||
padding: 0;
|
||||
margin: 1vh 1vh 0 0;
|
||||
} */
|
||||
|
||||
.camera-stats,
|
||||
.event-stats {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #001529;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* .bottom-part{
|
||||
box-sizing: border-box;
|
||||
height: 45vh;
|
||||
width: 86vw;
|
||||
|
||||
} */
|
||||
|
||||
.bottom-row {
|
||||
/* margin: 10px; */
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.code-status-chart {
|
||||
width: 84vw;
|
||||
height: 40vh;
|
||||
padding: 0vh 1vw 0 1vw;
|
||||
/* margin: 0vh 1vw; */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,411 +1,345 @@
|
||||
<template>
|
||||
<div class="camera-container">
|
||||
<!-- 左侧:摄像头列表 -->
|
||||
<div class="camera-list">
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索摄像头名称"
|
||||
prefix-icon="el-icon-search"
|
||||
clearable
|
||||
class="search-input"
|
||||
/>
|
||||
<el-card v-for="camera in filteredCameras" :key="camera.id" class="camera-item" @click="selectCamera(camera)">
|
||||
<div class="camera-header">
|
||||
<span>ID: {{ camera.id }}</span>
|
||||
<span class="status" :class="{ 'online': camera.status === 'online', 'offline': camera.status !== 'online' }">
|
||||
{{ camera.status === 'online' ? '在线' : '离线' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="camera-content">
|
||||
<img :src="camera.snapshot" alt="camera-preview" class="camera-thumbnail" />
|
||||
<p class="camera-name">{{ camera.name }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="list-view">
|
||||
<div class="background-overlay"></div>
|
||||
<div class="container">
|
||||
<!-- 头栏 -->
|
||||
<div class="header">
|
||||
<div class="title-left">
|
||||
<div small-bg>
|
||||
<dv-decoration8 :reverse="true" style="width:28vw;height:60px;" />
|
||||
</div>
|
||||
<div small-bg>
|
||||
<dv-decoration3 style="width:250px;height:30px;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:摄像头详情栅格 -->
|
||||
<div class="camera-grid">
|
||||
<div v-for="camera in selectedCameras" :key="camera.id" class="camera-details">
|
||||
<el-card class="camera-card">
|
||||
<div class="stream-control">
|
||||
<p class="camera-name-title">{{ camera.name }}</p>
|
||||
<el-button @click="closeStream(camera)" class="close-button" circle size="mini">X</el-button>
|
||||
<div class="line-title">
|
||||
<div text-2xl pt10>
|
||||
<div small-bg>
|
||||
<dv-decoration-11 class="custom-decoration" style="width:25vw;height:70px;">
|
||||
<div color-green font-700 bg="~ dark/0">
|
||||
<dv-decoration7 style="width:20vw;height:30px;">
|
||||
<div color-white font-400 >
|
||||
告警数据面板
|
||||
</div>
|
||||
</dv-decoration7>
|
||||
</div>
|
||||
</dv-decoration-11>
|
||||
</div>
|
||||
</div>
|
||||
<div small-bg>
|
||||
<dv-decoration5 :dur="2" style="width:25vw;height:80px;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="title-right">
|
||||
<div small-bg class="first-row">
|
||||
<dv-decoration8 style="width:28vw;height:60px;" />
|
||||
</div>
|
||||
<div small-bg class="second-row">
|
||||
<dv-decoration3 style="width:250px;height:30px;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频播放和快照部分 -->
|
||||
<div class="play-button-container" @mouseenter="showButton = true" @mouseleave="showButton = false">
|
||||
<!-- 未播放时显示快照或占位符 -->
|
||||
<div class="camera-placeholder" v-if="!camera.playing && !camera.snapshot">
|
||||
<el-icon size="48">
|
||||
<VideoCameraFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="main-section">
|
||||
<!-- 左侧区域 -->
|
||||
<div class="left-section">
|
||||
<!-- <div class="section top-left corner-style" >左上
|
||||
<div class="section hiden"></div>
|
||||
</div>
|
||||
<div class="section middle-left corner-style">左中
|
||||
<div class="section hiden"></div>
|
||||
</div>
|
||||
<div class="section bottom-left corner-style">左下
|
||||
<div class="section hiden"></div>
|
||||
</div> -->
|
||||
<dv-border-box-13 title="告警数据概览(数据计算数字)" class="section top-left">
|
||||
<LeftTop />
|
||||
</dv-border-box-13>
|
||||
<dv-border-box-13 title="点位告警数量(不同点位的数量)" class="section middle-left">不同点位告警的数量
|
||||
<LeftMiddle/>
|
||||
</dv-border-box-13>
|
||||
<!-- <dv-border-box-13 title="今日告警列表(告警详情)" class="section bottom-left ">今日告警列表(告警详情)
|
||||
|
||||
</dv-border-box-13> -->
|
||||
</div>
|
||||
|
||||
<img v-if="!camera.playing && camera.snapshot" :src="camera.snapshot" alt="camera snapshot"
|
||||
class="camera-snapshot" />
|
||||
<!-- 中部区域 -->
|
||||
<div class="center-section">
|
||||
<dv-border-box8 class="center-top">
|
||||
<CenterTop/>
|
||||
<!-- <dv-border-box8 class="center-top-header">警戒画面</dv-border-box8>
|
||||
<div class="center-top-grids">
|
||||
<div class="grid-item">栅格左上</div>
|
||||
<div class="grid-item">栅格右上</div>
|
||||
<div class="grid-item">栅格左下</div>
|
||||
<div class="grid-item">栅格右下</div>
|
||||
</div> -->
|
||||
</dv-border-box8>
|
||||
<div class="center-bottom">
|
||||
<!-- <dv-border-box-13 class="center-bottom-left">中下左</dv-border-box-13> -->
|
||||
<dv-border-box-13 class="center-bottom-right">告警数量分布情况
|
||||
<CenterBottom/>
|
||||
</dv-border-box-13>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 播放视频流的 canvas -->
|
||||
<canvas v-show="camera.playing" :ref="el => setCanvasRef(camera.id, el)" class="camera-large"></canvas>
|
||||
|
||||
<!-- 播放和暂停按钮 -->
|
||||
<el-button v-show="!camera.playing || showButton" class="play-button" type="primary" circle size="large"
|
||||
@click="handlePlayPause(camera)">
|
||||
<el-icon>
|
||||
<VideoPlay v-if="!camera.playing" />
|
||||
<VideoPause v-if="camera.playing" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<!-- 右侧区域 -->
|
||||
<div class="right-section">
|
||||
<dv-border-box-13 class="section top-right corner-style">时间段告警总数分布
|
||||
<RightTop/>
|
||||
</dv-border-box-13>
|
||||
<!-- <dv-border-box-13 class="section middle-right corner-style">告警数量分布</dv-border-box-13> -->
|
||||
<dv-border-box-13 class="section bottom-right corner-style">告警种类划分
|
||||
<LeftBottom/>
|
||||
</dv-border-box-13>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick,computed } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick, computed } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
|
||||
import LeftTop from '@/components/Max/LeftTop.vue';
|
||||
import LeftBottom from '@/components/Max/LeftBottom.vue';
|
||||
import LeftMiddle from '@/components/Max/LeftMiddle.vue';
|
||||
import RightTop from '@/components/Max/RightTop.vue';
|
||||
import CenterBottom from '@/components/Max/CenterBottom.vue';
|
||||
import CenterTop from '@/components/Max/CenterTop.vue';
|
||||
// import '/src/assets/viewListStyle.css'
|
||||
|
||||
// 存储所有摄像头列表
|
||||
const cameras = ref([]);
|
||||
// 存储已选择并展示的摄像头
|
||||
const selectedCameras = ref([]);
|
||||
// 控制播放按钮的显示状态
|
||||
const showButton = ref(false);
|
||||
// API 实例
|
||||
const apiInstance = new BoxApi();
|
||||
// 存储 canvas 引用
|
||||
const canvasRefs = ref({});
|
||||
|
||||
// 搜索输入的查询字符串
|
||||
const searchQuery = ref('');
|
||||
|
||||
// 计算属性:根据搜索关键词过滤摄像头列表
|
||||
const filteredCameras = computed(() =>
|
||||
cameras.value.filter(camera =>
|
||||
camera.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
// 获取摄像头列表
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const cameraData = await apiInstance.getMinCameras(token);
|
||||
cameras.value = cameraData;
|
||||
} catch (error) {
|
||||
console.error('获取摄像头列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 选择摄像头并展示详情
|
||||
const selectCamera = (camera) => {
|
||||
if (!selectedCameras.value.some(c => c.id === camera.id)) {
|
||||
selectedCameras.value.push({ ...camera, playing: false, streamPort: null });
|
||||
}
|
||||
};
|
||||
|
||||
// 设置 canvas 引用
|
||||
const setCanvasRef = (cameraId, el) => {
|
||||
if (el) {
|
||||
canvasRefs.value[cameraId] = el;
|
||||
}
|
||||
};
|
||||
|
||||
// 播放/暂停摄像头视频流
|
||||
const handlePlayPause = async (camera) => {
|
||||
if (camera.playing) {
|
||||
handleStopStream(camera);
|
||||
} else {
|
||||
handleStartStream(camera);
|
||||
}
|
||||
};
|
||||
|
||||
// 启动视频流
|
||||
const handleStartStream = async (camera) => {
|
||||
await nextTick(); // 确保 DOM 已渲染
|
||||
const canvas = canvasRefs.value[camera.id];
|
||||
|
||||
if (!camera || !canvas) {
|
||||
console.error('未找到对应的 canvas');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('alertToken');
|
||||
try {
|
||||
const response = await apiInstance.startCameraStream(token, camera.id);
|
||||
camera.streamPort = response.port;
|
||||
camera.playing = true;
|
||||
|
||||
const rememberedAddress = localStorage.getItem('rememberedAddress') || '127.0.0.1';
|
||||
const url = `ws://${rememberedAddress}:${camera.streamPort}/`;
|
||||
// const url = `ws://192.168.28.33:${camera.streamPort}/`;
|
||||
console.log('播放路径:', url);
|
||||
|
||||
if (window.JSMpeg) {
|
||||
const player = new window.JSMpeg.Player(url, {
|
||||
canvas: canvas,
|
||||
autoplay: true,
|
||||
videoBufferSize: 15 * 1024 * 1024,
|
||||
audioBufferSize: 5 * 1024 * 1024,
|
||||
});
|
||||
camera.player = player;
|
||||
} else {
|
||||
console.error('JSMpeg 未加载');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动视频流失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 停止视频流
|
||||
const handleStopStream = async (camera) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
try {
|
||||
await apiInstance.stopCameraStream(token, camera.id);
|
||||
camera.playing = false;
|
||||
|
||||
if (camera.player) {
|
||||
camera.player.destroy();
|
||||
camera.player = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止视频流失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭摄像头流视图
|
||||
const closeStream = (camera) => {
|
||||
handleStopStream(camera);
|
||||
selectedCameras.value = selectedCameras.value.filter(c => c.id !== camera.id);
|
||||
};
|
||||
|
||||
// 组件挂载时获取摄像头列表
|
||||
onMounted(() => {
|
||||
fetchCameras();
|
||||
});
|
||||
|
||||
// 组件卸载时清理资源
|
||||
onBeforeUnmount(() => {
|
||||
selectedCameras.value.forEach(camera => {
|
||||
if (camera.player) {
|
||||
camera.player.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.camera-container {
|
||||
.list-view {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background-color: #1E2E4A;
|
||||
}
|
||||
|
||||
/* 左侧摄像头列表 */
|
||||
.camera-list {
|
||||
width: 20%;
|
||||
min-width: 215px;
|
||||
max-height: 100vh;
|
||||
/* 限制高度为一屏 */
|
||||
overflow-y: auto;
|
||||
/* 超出时滚动 */
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #1E2E4A;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 10vh;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 每个摄像头项目的样式 */
|
||||
.camera-item {
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
padding: 12px;
|
||||
border: 1px solid #458388;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.camera-item:hover {
|
||||
background-color: #f5f7fa;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 摄像头项目头部 */
|
||||
.camera-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* 摄像头状态标签 */
|
||||
.status {
|
||||
margin-left: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.online {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.offline {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* 摄像头内容:缩略图和名称 */
|
||||
.camera-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.camera-thumbnail {
|
||||
width: 70px;
|
||||
height: 50px;
|
||||
margin-right: 10px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #12d1df;
|
||||
}
|
||||
|
||||
.camera-name {
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 右侧摄像头详情展示 */
|
||||
.camera-grid {
|
||||
width: 80%;
|
||||
height: 95vh; /* 占满页面右侧区域 */
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr); /* 两列 */
|
||||
grid-template-rows: repeat(2, 1fr); /* 两行 */
|
||||
gap: 5px; /* 栅格块之间的间距 */
|
||||
padding: 10px;
|
||||
/* background-color: #1E2E4A; */
|
||||
background: linear-gradient(to top, rgba(0, 3, 3, 0.4), rgba(9, 21, 196, 0.3));
|
||||
/* background: linear-gradient(to top, rgba(8, 53, 61, 0.4), rgba(9, 21, 196, 0.3)); */
|
||||
/* border: 2px solid #ece9e9; */
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.camera-details {
|
||||
height: 45vh;
|
||||
/* background-color: #3b2c2c; */
|
||||
/* background: linear-gradient(to top, rgba(8, 53, 61, 0.4), rgba(9, 21, 196, 0.3)); */
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* overflow-y: auto; */
|
||||
}
|
||||
.camera-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* background-color: #1E2E4A; */
|
||||
/* background: linear-gradient(to top, rgba(64, 226, 255, 0.7), rgba(211, 64, 248, 0.7)); */
|
||||
background: linear-gradient(to top, rgba(0, 7, 8, 0.9), rgba(12, 2, 155, 0.7));
|
||||
border: 2px solid #0b4c5f;
|
||||
border-radius: 8px;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
padding: 4vh 10vw 10vh 7vw;
|
||||
/* background-color: rgb(121, 184, 243); */
|
||||
background-color: #001529;
|
||||
/* background-image: url('/bg05.png'); */
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
/* background: radial-gradient(circle, rgb(24, 64, 197), rgb(0, 7, 60)); */
|
||||
position: relative;
|
||||
/* padding: 10px; */
|
||||
color: black;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.stream-control {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.custom-decoration{
|
||||
color: #70e5fa;;
|
||||
font-weight: bold;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
/* .stream-control p {
|
||||
color: #f5f7fa;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
font-weight: bold;
|
||||
|
||||
/* .background-overlay {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: url('/bg01.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
opacity: 0.8;
|
||||
z-index: 0;
|
||||
|
||||
} */
|
||||
|
||||
.camera-name-title {
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.camera-placeholder {
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
width: 80vw;
|
||||
height: 100%;
|
||||
color: #6b6b6b;
|
||||
border: 2px dashed rgba(109, 109, 109, 0.7);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||
background-color: rgba(50, 50, 50, 0.5);
|
||||
}
|
||||
|
||||
/* 播放中的 canvas 样式 */
|
||||
.camera-large {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
border: 2px solid rgba(109, 109, 109, 0.3);
|
||||
}
|
||||
|
||||
/* 摄像头大图和快照 */
|
||||
.camera-snapshot {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #dcdcdc;
|
||||
}
|
||||
|
||||
|
||||
/* 播放按钮容器 */
|
||||
.play-button-container {
|
||||
gap: 1vh;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
/* 为了在背景下显示 */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 12vh;
|
||||
width: 81vw;
|
||||
/* background-color: #000080; */
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 12vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.line-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* gap: 2vh; */
|
||||
}
|
||||
|
||||
.title-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title-right {
|
||||
display: flex;
|
||||
flex-direction: column; /* 设置为垂直布局 */
|
||||
}
|
||||
|
||||
.first-row {
|
||||
display: flex;
|
||||
justify-content: flex-start; /* 第一行内容靠左 */
|
||||
}
|
||||
|
||||
.second-row {
|
||||
display: flex;
|
||||
justify-content: flex-end; /* 第二行内容靠右 */
|
||||
|
||||
}
|
||||
|
||||
.main-section {
|
||||
height: 72vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1vw;
|
||||
}
|
||||
|
||||
.left-section,
|
||||
.right-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 22vw;
|
||||
height: 70vh;
|
||||
gap: 2vh;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.bottom-left,
|
||||
.top-right,
|
||||
/* .middle-right, */
|
||||
.bottom-right {
|
||||
color: white;
|
||||
text-align: center;
|
||||
width: 22vw;
|
||||
height: 22vh;
|
||||
/* background-color: rgba(255, 255, 255, 0.1); */
|
||||
/* border: 3px solid rgba(0, 255, 255, 0.5); */
|
||||
/* box-shadow: 0 2px 5px rgba(221, 204, 204, 0.5); */
|
||||
}
|
||||
|
||||
.top-left{
|
||||
position: relative;
|
||||
padding: 1vh 1vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
width: 20vw;
|
||||
height: 20vh;
|
||||
}
|
||||
|
||||
.middle-left{
|
||||
color: white;
|
||||
text-align: center;
|
||||
width: 22vw;
|
||||
height: 46vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.top-right{
|
||||
width: 22vw;
|
||||
height: 46vh;
|
||||
display: flex;
|
||||
/* padding: 1.5vh; */
|
||||
}
|
||||
|
||||
.bottom-left{
|
||||
/* color: white; */
|
||||
/* position: relative; */
|
||||
padding-left: 0.5vw;
|
||||
display: flex;
|
||||
/* justify-content: center; */
|
||||
/* align-items: center; */
|
||||
/* width: 20vw; */
|
||||
/* height: 20vh; */
|
||||
}
|
||||
|
||||
/* .top-left, .top-right {
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
|
||||
|
||||
.bottom-left, .bottom-right {
|
||||
margin-top: 1vh;
|
||||
} */
|
||||
|
||||
.center-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 35vw;
|
||||
height: 70vh;
|
||||
gap: 1vh;
|
||||
}
|
||||
|
||||
.center-top {
|
||||
height: 47vh;
|
||||
/* background-color: #555; */
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* .center-top-header {
|
||||
height: 3vh;
|
||||
width: 35vw;
|
||||
text-align: center;
|
||||
line-height: 3vh;
|
||||
margin-bottom: 1vh;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.center-top-grids {
|
||||
display: grid;
|
||||
grid-template-columns: 17vw 17vw;
|
||||
gap: 2vh 1vw;
|
||||
}
|
||||
.grid-item {
|
||||
width: 17vw;
|
||||
height: 20vh;
|
||||
background-color: #777;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
} */
|
||||
|
||||
.center-bottom {
|
||||
display: flex;
|
||||
gap: 1vw;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* 播放按钮 */
|
||||
.play-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
.center-bottom-left,
|
||||
.center-bottom-right {
|
||||
width: 35vw;
|
||||
height: 22vh;
|
||||
/* background-color: #444; */
|
||||
color: white;
|
||||
text-align: center;
|
||||
/* margin-top: 1vh; */
|
||||
}
|
||||
|
||||
.play-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,365 +1,180 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTab" class="tab-div">
|
||||
<el-tab-pane label="所有" name="all"></el-tab-pane>
|
||||
<el-tab-pane label="今天" name="today"></el-tab-pane>
|
||||
<el-tab-pane label="7天" name="week"></el-tab-pane>
|
||||
<el-tab-pane label="30天" name="month"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="settings-container">
|
||||
<el-row class="popup-row">
|
||||
<el-checkbox v-model="isPopupEnabled" @change="handleCheckboxChange" style="color: aliceblue;">
|
||||
开启弹窗
|
||||
</el-checkbox>
|
||||
</el-row>
|
||||
|
||||
<el-row class="top-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraAll />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
通道总数:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOnline />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
在线:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOnlineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOffline />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
离线:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOfflineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="bottom-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventAll />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
事件总数:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ eventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventClosed />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
已处理:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ closedEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventPending />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
未处理:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ pendintingEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="channel-row">
|
||||
<Channel />
|
||||
</el-row>
|
||||
|
||||
<!-- el-dialog 用于显示通知详情 -->
|
||||
<!-- <el-dialog
|
||||
title="告警详情"
|
||||
v-model="dialogVisible"
|
||||
width="50%"
|
||||
@close="handleDialogClose">
|
||||
<el-row :gutter="30" style="margin-bottom: 2vh;">
|
||||
<el-col span="8">告警编号:{{ dialogContent.id }}</el-col>
|
||||
<el-col span="24">摄像头编号:{{ dialogContent.camera_id }}</el-col>
|
||||
<el-col span="8">摄像头名称:{{ dialogContent.camera?.name }}</el-col>
|
||||
<el-col span="8">告警类型:{{ algorithmMap.get(dialogContent.types) || '未知类型' }}</el-col>
|
||||
<el-col span="8">告警时间:{{ dialogContent.started_at }}</el-col>
|
||||
</el-row>
|
||||
<img :src="dialogContent.snapshotUrl" alt="告警图片" v-if="dialogContent.snapshotUrl" style="max-width: 100%;" />
|
||||
<video v-if="dialogContent.videoUrl" :src="dialogContent.videoUrl" controls style="max-width: 100%;"></video>
|
||||
</el-dialog> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, inject, onMounted } from 'vue';
|
||||
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||
import type { GlobalWebSocket } from '../utils/useGlobalWebSocket';
|
||||
import Channel from '@/components/Channel.vue';
|
||||
import { setDialogHandler } from '../utils/useGlobalWebSocket';
|
||||
import { BoxApi } from '@/utils/boxApi';
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import CameraAll from '@/icons/CameraAll.vue';
|
||||
import CameraOnline from '@/icons/CameraOnline.vue';
|
||||
import CameraOffline from '@/icons/CameraOffline.vue';
|
||||
import EventAll from '@/icons/EventAll.vue';
|
||||
import EventClosed from '@/icons/EventClosed.vue';
|
||||
import EventPending from '@/icons/EventPending.vue';
|
||||
// const apiInstance = new BoxApi();
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const cameraCount = ref(0);
|
||||
const cameraOfflineCount = ref(0);
|
||||
const cameraOnlineCount = ref(0);
|
||||
const eventCount = ref(0);
|
||||
const pendintingEventCount = ref(0);
|
||||
const closedEventCount = ref(0);
|
||||
const isPopupEnabled = ref(false);
|
||||
const globalWebSocket = inject<GlobalWebSocket>('globalWebSocket');
|
||||
|
||||
const activeTab = ref('all');
|
||||
|
||||
const getTodayData = () => {
|
||||
const today = new Date();
|
||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfToday),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const getWeekData = () => {
|
||||
const today = new Date();
|
||||
const startOfWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfWeek),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const getMonthData = () => {
|
||||
const today = new Date();
|
||||
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfMonth),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
cameraCount.value = firstResponse.count;
|
||||
console.log("总数》》》》》》》》》》》》》", cameraCount.value)
|
||||
allCameras = firstResponse.results;
|
||||
// const dialogVisible = ref(false);
|
||||
// const dialogContent = ref({
|
||||
// id: null,
|
||||
// camera_id: null,
|
||||
// camera: { name: '' },
|
||||
// types: null,
|
||||
// started_at: null,
|
||||
// snapshotUrl: '',
|
||||
// videoUrl: ''
|
||||
// });
|
||||
// const algorithmMap = ref(new Map());
|
||||
|
||||
|
||||
const total = cameraCount.value;
|
||||
while (offset + limit < total) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
if (!globalWebSocket) {
|
||||
throw new Error('globalWebSocket 注入失败');
|
||||
}
|
||||
|
||||
allCameras.forEach((camera) => {
|
||||
if (camera.status === 'online') {
|
||||
cameraOnlineCount.value++;
|
||||
} else if (camera.status === 'offline') {
|
||||
cameraOfflineCount.value++;
|
||||
} else {
|
||||
exceptionCount.value++;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching cameras:', error);
|
||||
// 在页面加载时,从 localStorage 中读取勾选状态并注册 WebSocket 回调
|
||||
onMounted(async () => {
|
||||
const storedState = localStorage.getItem('isPopupEnabled');
|
||||
isPopupEnabled.value = storedState === 'true';
|
||||
if (isPopupEnabled.value && !globalWebSocket.isWebSocketConnected.value) {
|
||||
globalWebSocket.connectWebSocket();
|
||||
}
|
||||
|
||||
// try {
|
||||
// const token = localStorage.getItem('alertToken');
|
||||
// const algorithms = await apiInstance.getAlgorithms(token);
|
||||
|
||||
|
||||
// algorithmMap.value = new Map(algorithms.map((algo: { code_name: string, name: string }) => [algo.code_name, algo.name]));
|
||||
|
||||
|
||||
// setDialogHandler(showDialog);
|
||||
|
||||
|
||||
// if (isPopupEnabled.value && !globalWebSocket.isWebSocketConnected.value) {
|
||||
// globalWebSocket.connectWebSocket();
|
||||
// }
|
||||
// } catch (error) {
|
||||
// ElMessage.error('获取算法映射失败');
|
||||
// console.error(error);
|
||||
// }
|
||||
});
|
||||
|
||||
// 当用户勾选或取消勾选时触发的函数
|
||||
const handleCheckboxChange = () => {
|
||||
if (isPopupEnabled.value) {
|
||||
ElMessageBox.confirm('是否开启弹窗提示?', '提示', {
|
||||
confirmButtonText: '是',
|
||||
cancelButtonText: '否',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
localStorage.setItem('isPopupEnabled', 'true');
|
||||
globalWebSocket.connectWebSocket();
|
||||
})
|
||||
.catch(() => {
|
||||
isPopupEnabled.value = false;
|
||||
localStorage.setItem('isPopupEnabled', 'false');
|
||||
});
|
||||
} else {
|
||||
globalWebSocket.closeWebSocket();
|
||||
localStorage.setItem('isPopupEnabled', 'false');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// const fetchEvents = async (timeAfter = null, timeBefore = null) => {
|
||||
// const showDialog = async (data: any) => {
|
||||
// try {
|
||||
// const token = localStorage.getItem('alertToken');
|
||||
// const token = localStorage.getItem('alertToken');
|
||||
// const eventDetails = await apiInstance.getEventById(data.id, token);
|
||||
|
||||
// const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter);
|
||||
// eventCount.value = totalResponse.count;
|
||||
|
||||
// const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
|
||||
// pendintingEventCount.value = pendingResponse.count;
|
||||
|
||||
// const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'closed');
|
||||
// closedEventCount.value = closedResponse.count;
|
||||
// const snapshot = eventDetails.mediums.find((item: any) => item.name === 'snapshot');
|
||||
// const video = eventDetails.mediums.find((item: any) => item.name === 'video');
|
||||
|
||||
// dialogContent.value = {
|
||||
// id: eventDetails.id,
|
||||
// camera_id: eventDetails.camera_id,
|
||||
// camera: eventDetails.camera,
|
||||
// types: eventDetails.types,
|
||||
// started_at: eventDetails.started_at,
|
||||
// snapshotUrl: snapshot?.file || '',
|
||||
// videoUrl: video?.file || ''
|
||||
// };
|
||||
|
||||
// dialogVisible.value = true;
|
||||
// } catch (error) {
|
||||
// console.error('获取事件数据失败:', error);
|
||||
// ElMessage.error('获取告警详情失败');
|
||||
// console.error(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
let timeRange = { timeAfter: null, timeBefore: null };
|
||||
|
||||
// 根据 activeTab 设置时间范围
|
||||
if (activeTab.value === 'today') {
|
||||
timeRange = getTodayData();
|
||||
} else if (activeTab.value === 'week') {
|
||||
timeRange = getWeekData();
|
||||
} else if (activeTab.value === 'month') {
|
||||
timeRange = getMonthData();
|
||||
}
|
||||
|
||||
// 获取告警总数
|
||||
const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter);
|
||||
eventCount.value = totalResponse.count;
|
||||
|
||||
// 获取状态为 pending 的告警数量
|
||||
const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'pending');
|
||||
pendintingEventCount.value = pendingResponse.count;
|
||||
|
||||
// 获取状态为 closed 的告警数量
|
||||
const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'closed');
|
||||
closedEventCount.value = closedResponse.count;
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取事件数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(activeTab, (newTab) => {
|
||||
fetchEvents();
|
||||
});
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// getTodayData();
|
||||
// getWeekData();
|
||||
// getMonthData();
|
||||
fetchCameras();
|
||||
fetchEvents();
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
// const handleDialogClose = () => {
|
||||
// dialogContent.value = {
|
||||
// id: null,
|
||||
// camera_id: null,
|
||||
// camera: { name: '' },
|
||||
// types: null,
|
||||
// started_at: null,
|
||||
// snapshotUrl: '',
|
||||
// videoUrl: ''
|
||||
// };
|
||||
// dialogVisible.value = false;
|
||||
// };
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.top-row,
|
||||
.bottom-row {
|
||||
.settings-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #ffffff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
margin-bottom: 20px;
|
||||
height: 20vh;
|
||||
width: 80vw;
|
||||
padding: 1vh 1vw;
|
||||
margin: 1vh 2vw;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.channel-row {
|
||||
margin: 1vh 2vw;
|
||||
width: 80vw;
|
||||
height: 70vh;
|
||||
background-color: #001529;
|
||||
color: aliceblue;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.inner-count-text {
|
||||
color: rgb(91, 224, 241);
|
||||
}
|
||||
|
||||
.tab-div{
|
||||
background-color: #001529;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
margin-left: 1vh;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: #2ea0ec;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
/* width: 15vw; */
|
||||
position: static !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user