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

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>