Compare commits
8 Commits
b00dac74de
...
8a31a31d93
Author | SHA1 | Date |
---|---|---|
|
8a31a31d93 | |
|
0e79e6f9cd | |
|
689a167f4e | |
|
1b6a66302f | |
|
9b1a251ded | |
|
93f9af1c99 | |
|
8184d3ca94 | |
|
c8f02faaa7 |
|
@ -9,27 +9,42 @@
|
||||||
<el-tab-pane label="本周" name="second">
|
<el-tab-pane label="本周" name="second">
|
||||||
<div ref="weekLineChartDom" class="chart-container"></div> <!-- 本周图表 -->
|
<div ref="weekLineChartDom" class="chart-container"></div> <!-- 本周图表 -->
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="最近30天" name="third"> <!-- 新增最近30天选项卡 -->
|
||||||
|
<div ref="monthLineChartDom" class="chart-container"></div> <!-- 最近30天图表 -->
|
||||||
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from 'vue';
|
import { ref, onMounted, watch, nextTick } from 'vue';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
|
import { BoxApi } from '@/utils/boxApi.ts';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { BoxApi } from '@/utils/boxApi.ts'; // 使用 BoxApi
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
const activeName = ref('first');
|
const activeName = ref('first');
|
||||||
const hourlyCounts = ref(Array(24).fill(0)); // 初始化24小时的计数器
|
const hourlyCounts = ref(Array(24).fill(0)); // 初始化24小时的计数器
|
||||||
const dailyCounts = ref(Array(7).fill(0)); // 初始化7天的计数器
|
|
||||||
const weekDates = ref([]); // 存储前7天的日期
|
|
||||||
const chartInstanceToday = ref(null);
|
const chartInstanceToday = ref(null);
|
||||||
const chartInstanceWeek = ref(null);
|
|
||||||
const todayLineChartDom = ref(null);
|
const todayLineChartDom = ref(null);
|
||||||
|
|
||||||
|
const dailyCounts = ref(Array(7).fill(0)); // 初始化7天的计数器
|
||||||
|
const weekDates = ref([]); // 存储前7天的日期
|
||||||
|
const chartInstanceWeek = ref(null);
|
||||||
const weekLineChartDom = ref(null);
|
const weekLineChartDom = ref(null);
|
||||||
|
|
||||||
const apiInstance = new BoxApi(); // 创建 BoxApi 实例
|
const monthlyCounts = ref(Array(30).fill(0)); // 初始化30天的计数器
|
||||||
|
const monthDates = ref([]); // 存储前30天的日期
|
||||||
|
const chartInstanceMonth = ref(null); // 30天的图表实例
|
||||||
|
const monthLineChartDom = ref(null);
|
||||||
|
|
||||||
|
const apiInstance = new BoxApi();
|
||||||
|
const allEvents = ref([]); // 使用响应式 allEvents
|
||||||
|
|
||||||
|
let isTodayChartInitialized = false;
|
||||||
|
let isWeekChartInitialized = false;
|
||||||
|
let isMonthChartInitialized = false; // 30天图表是否初始化
|
||||||
|
|
||||||
// 计算前7天的日期
|
// 计算前7天的日期
|
||||||
const calculateWeekDates = () => {
|
const calculateWeekDates = () => {
|
||||||
|
@ -37,6 +52,84 @@ const calculateWeekDates = () => {
|
||||||
weekDates.value = Array.from({ length: 7 }, (_, i) => today.subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
weekDates.value = Array.from({ length: 7 }, (_, i) => today.subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 计算前30天的日期
|
||||||
|
const calculateMonthDates = () => {
|
||||||
|
const today = dayjs();
|
||||||
|
monthDates.value = Array.from({ length: 30 }, (_, i) => today.subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化今日图表
|
||||||
|
const initTodayChart = () => {
|
||||||
|
if (todayLineChartDom.value) {
|
||||||
|
chartInstanceToday.value = echarts.init(todayLineChartDom.value);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: '今日告警数量趋势',
|
||||||
|
top: '3%',
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'cross',
|
||||||
|
},
|
||||||
|
formatter: function (params) {
|
||||||
|
const { value, name } = params[0];
|
||||||
|
return `${name}<br/>告警数量: ${value}`;
|
||||||
|
},
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`),
|
||||||
|
axisLabel: {
|
||||||
|
rotate: 0,
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
min: 0,
|
||||||
|
minInterval: 1,
|
||||||
|
axisLabel: {
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '告警数量',
|
||||||
|
type: 'line',
|
||||||
|
data: hourlyCounts.value.length > 0 ? hourlyCounts.value : Array(24).fill(0),
|
||||||
|
smooth: true,
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgb(60,178,239)', // 曲线颜色
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: 'rgba(74, 172, 178, 0.3)', // 覆盖面积颜色
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
chartInstanceToday.value.setOption(option);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化本周图表
|
// 初始化本周图表
|
||||||
const initWeekChart = () => {
|
const initWeekChart = () => {
|
||||||
if (weekLineChartDom.value) {
|
if (weekLineChartDom.value) {
|
||||||
|
@ -49,13 +142,13 @@ const initWeekChart = () => {
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
type: 'cross'
|
type: 'cross',
|
||||||
},
|
},
|
||||||
formatter: function (params) {
|
formatter: function (params) {
|
||||||
const { value } = params[0];
|
const { value } = params[0];
|
||||||
|
@ -63,19 +156,19 @@ const initWeekChart = () => {
|
||||||
},
|
},
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 12
|
fontSize: 12,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
boundaryGap: true,
|
boundaryGap: true,
|
||||||
data: weekDates.value.length > 0 ? weekDates.value : Array.from({ length: 7 }, (_, i) => `日期 ${i+1}`),
|
data: weekDates.value.length > 0 ? weekDates.value : Array.from({ length: 7 }, (_, i) => `日期 ${i + 1}`),
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
rotate: 0,
|
rotate: 0,
|
||||||
interval: 0,
|
interval: 0,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 12
|
fontSize: 12,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
@ -83,8 +176,8 @@ const initWeekChart = () => {
|
||||||
minInterval: 1,
|
minInterval: 1,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 12
|
fontSize: 12,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
|
@ -97,35 +190,42 @@ const initWeekChart = () => {
|
||||||
position: 'top',
|
position: 'top',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 12
|
fontSize: 12,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
lineStyle: {
|
||||||
]
|
color: 'rgb(60,178,239)', // 曲线颜色
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: 'rgba(74, 172, 178, 0.3)', // 覆盖面积颜色
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
chartInstanceWeek.value.setOption(option);
|
chartInstanceWeek.value.setOption(option);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化今日图表
|
// 初始化最近30天图表
|
||||||
const initTodayChart = () => {
|
const initMonthChart = () => {
|
||||||
if (todayLineChartDom.value) {
|
if (monthLineChartDom.value) {
|
||||||
chartInstanceToday.value = echarts.init(todayLineChartDom.value);
|
chartInstanceMonth.value = echarts.init(monthLineChartDom.value);
|
||||||
|
|
||||||
const option = {
|
const option = {
|
||||||
title: {
|
title: {
|
||||||
text: '今日告警数量趋势',
|
text: '最近30天告警数量趋势',
|
||||||
top: '3%',
|
top: '3%',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
type: 'cross'
|
type: 'cross',
|
||||||
},
|
},
|
||||||
formatter: function (params) {
|
formatter: function (params) {
|
||||||
const { value, name } = params[0];
|
const { value, name } = params[0];
|
||||||
|
@ -133,17 +233,17 @@ const initTodayChart = () => {
|
||||||
},
|
},
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontSize: 12
|
fontSize: 12,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
data: Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`),
|
data: monthDates.value,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
rotate: 0,
|
rotate: 0,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
@ -151,38 +251,44 @@ const initTodayChart = () => {
|
||||||
minInterval: 1,
|
minInterval: 1,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: '告警数量',
|
name: '告警数量',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: hourlyCounts.value.length > 0 ? hourlyCounts.value : Array(24).fill(0),
|
data: monthlyCounts.value.length > 0 ? monthlyCounts.value : Array(30).fill(0),
|
||||||
smooth: true,
|
smooth: true,
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
lineStyle: {
|
||||||
]
|
color: 'rgb(60,178,239)', // 曲线颜色
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: 'rgba(74, 172, 178, 0.3)', // 覆盖面积颜色
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
chartInstanceToday.value.setOption(option);
|
chartInstanceMonth.value.setOption(option);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 分批获取并处理告警数据
|
// 获取并处理告警数据
|
||||||
const fetchAndProcessEvents = async (token) => {
|
const fetchAndProcessEvents = async (token) => {
|
||||||
try {
|
try {
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
const pageSize = 2000; // 每次加载100条数据
|
const pageSize = 1000;
|
||||||
let allEvents = [];
|
|
||||||
|
|
||||||
// 第一次请求,获取告警总数
|
// 第一次请求,获取告警总数
|
||||||
const { tableData: firstBatch, totalItems } = await apiInstance.getEvents(token, pageSize, currentPage);
|
const { tableData: firstBatch, totalItems } = await apiInstance.getEvents(token, pageSize, currentPage);
|
||||||
allEvents = [...firstBatch];
|
allEvents.value = [...firstBatch]; // 使用 allEvents.value
|
||||||
|
|
||||||
// 根据告警总数逐页加载所有数据
|
// 根据告警总数逐页加载所有数据
|
||||||
const totalPages = Math.ceil(totalItems / pageSize);
|
const totalPages = Math.ceil(totalItems / pageSize);
|
||||||
|
@ -191,42 +297,46 @@ const fetchAndProcessEvents = async (token) => {
|
||||||
while (currentPage < totalPages) {
|
while (currentPage < totalPages) {
|
||||||
currentPage++;
|
currentPage++;
|
||||||
const { tableData: nextBatch } = await apiInstance.getEvents(token, pageSize, currentPage);
|
const { tableData: nextBatch } = await apiInstance.getEvents(token, pageSize, currentPage);
|
||||||
allEvents = [...allEvents, ...nextBatch];
|
allEvents.value = [...allEvents.value, ...nextBatch]; // 使用 allEvents.value
|
||||||
|
|
||||||
// 每次加载数据后逐步更新图表
|
|
||||||
processEventData(allEvents);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最终处理全部数据
|
// 处理数据
|
||||||
processEventData(allEvents);
|
processEventData(allEvents.value); // 今日告警数据
|
||||||
|
processWeekData(); // 本周告警数据
|
||||||
|
processMonthData(); // 最近30天告警数据
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching events:", error);
|
console.error("Error fetching events:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理告警数据并更新图表
|
// 处理今日告警数据并更新图表
|
||||||
const processEventData = (events) => {
|
const processEventData = (events) => {
|
||||||
// 重置计数器
|
|
||||||
hourlyCounts.value = Array(24).fill(0);
|
hourlyCounts.value = Array(24).fill(0);
|
||||||
dailyCounts.value = Array(7).fill(0);
|
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||||
|
|
||||||
// 统计数据
|
events.forEach((event) => {
|
||||||
events.forEach(event => {
|
|
||||||
const endedAt = event.ended_at;
|
const endedAt = event.ended_at;
|
||||||
if (endedAt) {
|
if (endedAt) {
|
||||||
const endedDate = new Date(endedAt);
|
const endedDate = new Date(endedAt);
|
||||||
|
|
||||||
// 统计今日每小时的告警数量
|
|
||||||
if (endedDate >= startOfToday && endedDate < endOfToday) {
|
if (endedDate >= startOfToday && endedDate < endOfToday) {
|
||||||
const hour = endedDate.getHours();
|
const hour = endedDate.getHours();
|
||||||
hourlyCounts.value[hour] += 1;
|
hourlyCounts.value[hour] += 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 统计最近7天的告警数量
|
updateCharts();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理本周告警数据
|
||||||
|
const processWeekData = () => {
|
||||||
|
dailyCounts.value = Array(7).fill(0);
|
||||||
|
|
||||||
|
allEvents.value.forEach((event) => {
|
||||||
|
const endedAt = event.ended_at;
|
||||||
|
if (endedAt) {
|
||||||
const endedDay = dayjs(endedAt).format('YYYY-MM-DD');
|
const endedDay = dayjs(endedAt).format('YYYY-MM-DD');
|
||||||
weekDates.value.forEach((date, index) => {
|
weekDates.value.forEach((date, index) => {
|
||||||
if (endedDay === date) {
|
if (endedDay === date) {
|
||||||
|
@ -236,72 +346,145 @@ const processEventData = (events) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新图表
|
updateWeekChart();
|
||||||
updateCharts();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新图表
|
// 处理最近30天告警数据
|
||||||
|
const processMonthData = () => {
|
||||||
|
monthlyCounts.value = Array(30).fill(0);
|
||||||
|
|
||||||
|
allEvents.value.forEach((event) => {
|
||||||
|
const endedAt = event.ended_at;
|
||||||
|
if (endedAt) {
|
||||||
|
const endedDay = dayjs(endedAt).format('YYYY-MM-DD');
|
||||||
|
monthDates.value.forEach((date, index) => {
|
||||||
|
if (endedDay === date) {
|
||||||
|
monthlyCounts.value[index] += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateMonthChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新今日图表
|
||||||
const updateCharts = () => {
|
const updateCharts = () => {
|
||||||
if (chartInstanceToday.value) {
|
if (chartInstanceToday.value) {
|
||||||
chartInstanceToday.value.setOption({
|
chartInstanceToday.value.setOption({
|
||||||
series: [{ data: hourlyCounts.value }]
|
series: [{ data: hourlyCounts.value }],
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chartInstanceWeek.value) {
|
|
||||||
chartInstanceWeek.value.setOption({
|
|
||||||
series: [{ data: dailyCounts.value }]
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 更新本周图表
|
||||||
|
const updateWeekChart = () => {
|
||||||
|
if (chartInstanceWeek.value) {
|
||||||
|
chartInstanceWeek.value.setOption({
|
||||||
|
series: [{ data: dailyCounts.value }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新最近30天图表
|
||||||
|
const updateMonthChart = () => {
|
||||||
|
if (chartInstanceMonth.value) {
|
||||||
|
chartInstanceMonth.value.setOption({
|
||||||
|
series: [{ data: monthlyCounts.value }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理选项卡切换
|
||||||
|
const handleClick = async (tab) => {
|
||||||
|
if (tab.props.name === 'first') {
|
||||||
|
if (!isTodayChartInitialized) {
|
||||||
|
await nextTick();
|
||||||
|
initTodayChart();
|
||||||
|
isTodayChartInitialized = true;
|
||||||
|
}
|
||||||
|
delayInstanceToday();
|
||||||
|
} else if (tab.props.name === 'second') {
|
||||||
|
await processWeekData();
|
||||||
|
if (!isWeekChartInitialized) {
|
||||||
|
await nextTick();
|
||||||
|
initWeekChart();
|
||||||
|
isWeekChartInitialized = true;
|
||||||
|
}
|
||||||
|
delayInstanceWeek();
|
||||||
|
} else if (tab.props.name === 'third') {
|
||||||
|
await processMonthData();
|
||||||
|
if (!isMonthChartInitialized) {
|
||||||
|
await nextTick();
|
||||||
|
initMonthChart();
|
||||||
|
isMonthChartInitialized = true;
|
||||||
|
}
|
||||||
|
delayInstanceMonth();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 窗口调整时重新渲染图表
|
||||||
|
const delayInstanceToday = debounce(() => {
|
||||||
|
if (chartInstanceToday.value) {
|
||||||
|
chartInstanceToday.value.resize();
|
||||||
|
updateCharts();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
const delayInstanceWeek = debounce(() => {
|
||||||
|
if (chartInstanceWeek.value) {
|
||||||
|
chartInstanceWeek.value.resize();
|
||||||
|
updateWeekChart();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
const delayInstanceMonth = debounce(() => {
|
||||||
|
if (chartInstanceMonth.value) {
|
||||||
|
chartInstanceMonth.value.resize();
|
||||||
|
updateMonthChart();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
// 组件挂载时调用
|
// 组件挂载时调用
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
calculateWeekDates(); // 计算前7天日期
|
calculateWeekDates(); // 计算前7天日期
|
||||||
|
calculateMonthDates(); // 计算前30天日期
|
||||||
const token = localStorage.getItem('alertToken');
|
const token = localStorage.getItem('alertToken');
|
||||||
await fetchAndProcessEvents(token);
|
await fetchAndProcessEvents(token); // 加载数据
|
||||||
|
|
||||||
// 初始化图表
|
|
||||||
initTodayChart();
|
initTodayChart();
|
||||||
initWeekChart();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听hourlyCounts,当其变化时更新今日图表
|
// 监听数据变化,更新对应的图表
|
||||||
watch(hourlyCounts, () => {
|
watch(hourlyCounts, () => {
|
||||||
if (chartInstanceToday.value) {
|
if (chartInstanceToday.value) {
|
||||||
chartInstanceToday.value.setOption({
|
chartInstanceToday.value.setOption({
|
||||||
series: [{
|
series: [{ data: hourlyCounts.value }],
|
||||||
data: hourlyCounts.value
|
|
||||||
}]
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听dailyCounts,当其变化时更新本周图表
|
|
||||||
watch(dailyCounts, () => {
|
watch(dailyCounts, () => {
|
||||||
if (chartInstanceWeek.value) {
|
if (chartInstanceWeek.value) {
|
||||||
chartInstanceWeek.value.setOption({
|
chartInstanceWeek.value.setOption({
|
||||||
series: [{
|
series: [{ data: dailyCounts.value }],
|
||||||
data: dailyCounts.value
|
|
||||||
}]
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// tab点击处理函数
|
watch(monthlyCounts, () => {
|
||||||
const handleClick = (tab) => {
|
if (chartInstanceMonth.value) {
|
||||||
if (tab.name === 'first') {
|
chartInstanceMonth.value.setOption({
|
||||||
initTodayChart(); // 切换到今日,重新加载今日图表
|
series: [{ data: monthlyCounts.value }],
|
||||||
} else if (tab.name === 'second') {
|
});
|
||||||
initWeekChart(); // 切换到本周,重新加载本周图表
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.alert-card {
|
.alert-card {
|
||||||
background-color: #2a3f54;
|
background-color: #304555;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -314,20 +497,15 @@ const handleClick = (tab) => {
|
||||||
border-bottom: 1px solid #3a4b5c;
|
border-bottom: 1px solid #3a4b5c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-tabs {
|
.chart-container {
|
||||||
margin-bottom: 0;
|
height: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-tabs__item {
|
::v-deep .el-tabs__item {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
::v-deep .el-tabs__item.is-active {
|
||||||
width: 780px;
|
color: #2ea0ec;
|
||||||
height: 420px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tabs__active-bar {
|
|
||||||
background-color: #409EFF;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,238 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-card class="camera-card">
|
||||||
|
<el-row :gutter="20" class="camera-row">
|
||||||
|
<!-- 左侧块:通道列表 -->
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card class="channel-card" shadow="hover">
|
||||||
|
<div class="section-title">通道</div>
|
||||||
|
<div class="scroll-container">
|
||||||
|
<el-table :data="cameras" style="width: 100%;" height="250">
|
||||||
|
<el-table-column prop="id" label="ID" width="100"></el-table-column>
|
||||||
|
<el-table-column prop="name" label="名称"></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 中间块:摄像头总数量 -->
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card class="count-card" shadow="hover">
|
||||||
|
<div class="section-title">摄像数量</div>
|
||||||
|
<div class="camera-count-chart"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 右侧块:在线/离线/异常状态 -->
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card class="online-card" shadow="hover">
|
||||||
|
<div class="section-title">在线情况</div>
|
||||||
|
<div class="status-summary-chart"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
import { BoxApi } from '@/utils/boxApi.ts';
|
||||||
|
|
||||||
|
const cameras = ref([]);
|
||||||
|
const cameraCount = ref(0);
|
||||||
|
const onlineCount = ref(0);
|
||||||
|
const offlineCount = ref(0);
|
||||||
|
const exceptionCount = ref(0);
|
||||||
|
|
||||||
|
const apiInstance = new BoxApi();
|
||||||
|
|
||||||
|
// 分页获取摄像头数据,直到获取所有摄像头数据
|
||||||
|
const fetchCameras = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('alertToken');
|
||||||
|
const limit = 20; // 每次请求 20 条数据
|
||||||
|
let offset = 0; // 从 0 开始
|
||||||
|
let allCameras = [];
|
||||||
|
|
||||||
|
// 第一次请求,用于获取总数
|
||||||
|
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||||
|
cameraCount.value = firstResponse.count;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
cameras.value = allCameras;
|
||||||
|
|
||||||
|
// 统计在线、离线和异常的摄像头数量
|
||||||
|
allCameras.forEach(camera => {
|
||||||
|
if (camera.status === 'online') {
|
||||||
|
onlineCount.value++;
|
||||||
|
} else if (camera.status === 'offline') {
|
||||||
|
offlineCount.value++;
|
||||||
|
} else {
|
||||||
|
exceptionCount.value++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化中间的摄像头总数圆环图
|
||||||
|
initCountChart(cameraCount.value);
|
||||||
|
|
||||||
|
// 初始化右侧的在线/离线/异常圆环图
|
||||||
|
initStatusSummaryChart(onlineCount.value, offlineCount.value, exceptionCount.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching cameras:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化摄像头总数圆环图
|
||||||
|
const initCountChart = (count) => {
|
||||||
|
const chartDom = document.querySelector('.camera-count-chart');
|
||||||
|
const myChart = echarts.init(chartDom);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '60%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'center',
|
||||||
|
formatter: '{c}',
|
||||||
|
fontSize: 24,
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{ value: count, name: '总数' }
|
||||||
|
],
|
||||||
|
itemStyle: {
|
||||||
|
color: 'rgba(80,160,225, 1)'
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
myChart.setOption(option);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化在线/离线/异常状态圆环图
|
||||||
|
const initStatusSummaryChart = (online, offline, exception) => {
|
||||||
|
const chartDom = document.querySelector('.status-summary-chart');
|
||||||
|
const myChart = echarts.init(chartDom);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '60%'],
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
formatter: '{b}: {c} ({d}%)'
|
||||||
|
},
|
||||||
|
data: [
|
||||||
|
{ value: online, name: '在线' },
|
||||||
|
{ value: offline, name: '离线' },
|
||||||
|
{ value: exception, name: '异常' }
|
||||||
|
],
|
||||||
|
itemStyle: {
|
||||||
|
color: 'rgba(5,192,145, 1)'
|
||||||
|
// color: 'rgba(255,151,75, 1)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
myChart.setOption(option);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchCameras();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.camera-card {
|
||||||
|
/* background-color: #abcef1; */
|
||||||
|
/* background-color: linear-gradient(to top, rgba(150, 206, 243, 0.2), rgba(214, 240, 250, 0.3)); */
|
||||||
|
color: #fff;
|
||||||
|
/* border-radius: 8px; */
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-card {
|
||||||
|
background: linear-gradient(to top, rgba(150, 206, 243, 1), rgba(125, 29, 235, 0.3));
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-card {
|
||||||
|
background: linear-gradient(to bottom, rgba(246, 130, 85, 1), rgba(252, 186, 38, 0.3));
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-card {
|
||||||
|
background: linear-gradient(to top, rgba(112, 158, 228, 1), rgba(87, 204, 243, 0.3));
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-header {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 1px solid #3a4b5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-row {
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-card {
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
background-color: azure
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-container {
|
||||||
|
height: 180px;
|
||||||
|
/* overflow-y: scroll; */
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .scroll-container::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.status-summary-chart,
|
||||||
|
.camera-count-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table th {
|
||||||
|
background-color: #f5f7fa; /* 表头背景色 */
|
||||||
|
color: #333; /* 表头字体颜色 */
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center; /* 居中对齐 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -22,11 +22,15 @@
|
||||||
<template #title><span>告警列表</span></template>
|
<template #title><span>告警列表</span></template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/dataStatistics">
|
<el-menu-item index="/dataStatistics">
|
||||||
<el-icon><TrendCharts /></el-icon>
|
<el-icon>
|
||||||
|
<TrendCharts />
|
||||||
|
</el-icon>
|
||||||
<template #title><span>数据统计</span></template>
|
<template #title><span>数据统计</span></template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/userList">
|
<el-menu-item index="/userList">
|
||||||
<el-icon><Avatar /></el-icon>
|
<el-icon>
|
||||||
|
<Avatar />
|
||||||
|
</el-icon>
|
||||||
<template #title><span>用户管理</span></template>
|
<template #title><span>用户管理</span></template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-sub-menu index="1">
|
<el-sub-menu index="1">
|
||||||
|
@ -36,6 +40,12 @@
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>面板测试</span>
|
<span>面板测试</span>
|
||||||
</template>
|
</template>
|
||||||
|
<el-menu-item index="/test">
|
||||||
|
<el-icon>
|
||||||
|
<WarningFilled />
|
||||||
|
</el-icon>
|
||||||
|
<template #title><span>布局备份</span></template>
|
||||||
|
</el-menu-item>
|
||||||
<el-menu-item index="/alertChart">
|
<el-menu-item index="/alertChart">
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<WarningFilled />
|
<WarningFilled />
|
||||||
|
@ -48,6 +58,12 @@
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<template #title><span>功能点2测试</span></template>
|
<template #title><span>功能点2测试</span></template>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/cameras">
|
||||||
|
<el-icon>
|
||||||
|
<document />
|
||||||
|
</el-icon>
|
||||||
|
<template #title><span>功能点3测试</span></template>
|
||||||
|
</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
@ -238,7 +254,8 @@ const onLogout = async () => {
|
||||||
|
|
||||||
/* 头部样式 */
|
/* 头部样式 */
|
||||||
.nav-header {
|
.nav-header {
|
||||||
background-color: #001529;
|
/* background-color: #001529; */
|
||||||
|
background: linear-gradient(to right, rgba(0, 21, 41, 1), rgb(2, 16, 99,0.9));
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -268,6 +285,8 @@ const onLogout = async () => {
|
||||||
background-color: #f5f8fc;
|
background-color: #f5f8fc;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-height: 80%;
|
min-height: 80%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页脚样式 */
|
/* 页脚样式 */
|
||||||
|
@ -280,5 +299,8 @@ const onLogout = async () => {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
/* flex-shrink: 0; */
|
/* flex-shrink: 0; */
|
||||||
}</style>
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -44,7 +44,7 @@ const fetchTypeMapping = async (token) => {
|
||||||
const fetchAndProcessEvents = async (token) => {
|
const fetchAndProcessEvents = async (token) => {
|
||||||
try {
|
try {
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
const pageSize = 2000; // 每次加载 2000 条
|
const pageSize = 1000; // 每次加载 2000 条
|
||||||
let allEvents = [];
|
let allEvents = [];
|
||||||
|
|
||||||
// 第一次请求,获取告警总数和首批数据
|
// 第一次请求,获取告警总数和首批数据
|
||||||
|
@ -110,6 +110,7 @@ const initChart = () => {
|
||||||
},
|
},
|
||||||
itemGap: 20,
|
itemGap: 20,
|
||||||
data: seriesData.value.map(item => item.name),
|
data: seriesData.value.map(item => item.name),
|
||||||
|
show : true
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
|
@ -118,6 +119,7 @@ const initChart = () => {
|
||||||
radius: '50%',
|
radius: '50%',
|
||||||
center: ['50%', '150px'],
|
center: ['50%', '150px'],
|
||||||
data: seriesData.value,
|
data: seriesData.value,
|
||||||
|
data: [],
|
||||||
emphasis: {
|
emphasis: {
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
shadowBlur: 10,
|
shadowBlur: 10,
|
||||||
|
@ -126,7 +128,7 @@ const initChart = () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: false,
|
||||||
},
|
},
|
||||||
stillShowZeroSum: false,
|
stillShowZeroSum: false,
|
||||||
}
|
}
|
||||||
|
@ -171,15 +173,20 @@ onMounted(async () => {
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.stats-card {
|
.stats-card {
|
||||||
background-color: #2a3f54;
|
background-color: #304555;
|
||||||
|
/* background: linear-gradient(to top, rgba(16, 84, 194, 0.6), rgba(31, 48, 207, 0.7)); */
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
/* height: 100vh; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-header {
|
.stats-header {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-bottom: 1px solid #3a4b5c;
|
border-bottom: 1px solid #3a4b5c;
|
||||||
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-row {
|
.stats-row {
|
||||||
|
@ -189,6 +196,7 @@ onMounted(async () => {
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 445px;
|
height: 380px;
|
||||||
|
/* height: 445px; */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="alert-container">
|
<div class="alert-container">
|
||||||
<el-row class="top-pan">
|
<!-- <el-row class="top-pan">
|
||||||
<el-col :sm="24" :md="12" class="panel-section">
|
<el-col :sm="24" :md="12" class="panel-section">
|
||||||
<statistics />
|
<statistics />
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :sm="24" :md="12" class="panel-section">
|
<el-col :sm="24" :md="12" class="panel-section">
|
||||||
<alertChart />
|
<alertChart />
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row> -->
|
||||||
<el-row class="middle-row">
|
<el-row class="middle-row">
|
||||||
<span>
|
<span>
|
||||||
告警总数:{{ totalItems }}
|
告警总数:{{ displayTotalItems }}
|
||||||
</span>
|
</span>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row class="table-row">
|
<el-row class="table-row">
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<el-table :data="tableData" style="width:100%; height: 100%;" header-row-class-name="table-header"
|
<el-table :data="tableData" style="width:100%; height: 100%;" header-row-class-name="table-header" :fit="true">
|
||||||
:fit="true">
|
|
||||||
<el-table-column prop="id" label="告警编号" min-width="100"></el-table-column>
|
<el-table-column prop="id" label="告警编号" min-width="100"></el-table-column>
|
||||||
<el-table-column label="告警类型" min-width="150">
|
<el-table-column label="告警类型" min-width="150">
|
||||||
<template v-slot="scope">
|
<template v-slot="scope">
|
||||||
|
@ -82,7 +81,7 @@
|
||||||
<p>备注: {{ selectedRow.note }}</p>
|
<p>备注: {{ selectedRow.note }}</p>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<div class="event-media">
|
<!-- <div class="event-media">
|
||||||
<div v-for="medium in selectedRow.mediums" :key="medium.id" class="media-container">
|
<div v-for="medium in selectedRow.mediums" :key="medium.id" class="media-container">
|
||||||
<div v-if="medium.name === 'video'" class="media-item video-item">
|
<div v-if="medium.name === 'video'" class="media-item video-item">
|
||||||
<p>告警关联视频</p>
|
<p>告警关联视频</p>
|
||||||
|
@ -93,19 +92,40 @@
|
||||||
<el-image :src="medium.file" fit="contain"></el-image>
|
<el-image :src="medium.file" fit="contain"></el-image>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div> -->
|
||||||
|
<div class="event-media" :class="{ 'center-media' :mediums.length === 1 }">
|
||||||
|
<!-- 告警关联视频 -->
|
||||||
|
<div v-if="hasVideo" class="media-container video-item">
|
||||||
|
<p>告警关联视频</p>
|
||||||
|
<video :src="videoFile" controls @click="openMediaDialog('video', videoFile)"></video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 告警关联图片 -->
|
||||||
|
<div v-if="hasSnapshot" class="media-container image-item">
|
||||||
|
<p>告警关联图片</p>
|
||||||
|
<el-image :src="snapshotFile" fit="contain" @click="openMediaDialog('image', snapshotFile)"></el-image>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span slot="footer" class="dialog-footer">
|
<span slot="footer" class="dialog-footer">
|
||||||
<div class=""></div>
|
<div class=""></div>
|
||||||
</span>
|
</span>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
<el-dialog v-model="mediaDialogVisible" width="70%">
|
||||||
|
<div v-if="mediaType === 'image'">
|
||||||
|
<el-image :src="mediaSrc" fit="contain" style="width: 100%;"></el-image>
|
||||||
|
</div>
|
||||||
|
<div v-if="mediaType === 'video'">
|
||||||
|
<video :src="mediaSrc" controls style="width: 100%;"></video>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue';
|
import { ref, reactive, onMounted, computed } from 'vue';
|
||||||
import Statistics from '@/components/Statistics.vue';
|
// import Statistics from '@/components/Statistics.vue';
|
||||||
import AlertChart from '@/components/AlertChart.vue';
|
// import AlertChart from '@/components/AlertChart.vue';
|
||||||
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
||||||
|
|
||||||
// 创建 BoxApi 实例
|
// 创建 BoxApi 实例
|
||||||
|
@ -114,6 +134,10 @@ const boxApi = new BoxApi();
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const tableData = ref([]);
|
const tableData = ref([]);
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
|
const mediaDialogVisible = ref(false);
|
||||||
|
const mediaType = ref('');
|
||||||
|
const mediaSrc = ref('');
|
||||||
|
|
||||||
const selectedRow = ref({});
|
const selectedRow = ref({});
|
||||||
const mediums = ref([]);
|
const mediums = ref([]);
|
||||||
const duration = ref('');
|
const duration = ref('');
|
||||||
|
@ -124,8 +148,31 @@ const statusMapping = {
|
||||||
};
|
};
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const pageSize = ref(20);
|
const pageSize = ref(20);
|
||||||
const totalItems = ref(0);
|
|
||||||
const token = ref(null);
|
const token = ref(null);
|
||||||
|
const totalItems = ref(0);
|
||||||
|
const displayTotalItems = ref(0); // 用于展示的数字
|
||||||
|
|
||||||
|
|
||||||
|
const openMediaDialog = (type, src) => {
|
||||||
|
mediaType.value = type; // 设置媒体类型
|
||||||
|
mediaSrc.value = src; // 设置媒体源文件
|
||||||
|
mediaDialogVisible.value = true; // 打开弹窗
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算属性:根据 mediums 动态判断是否存在视频和图片
|
||||||
|
const hasVideo = computed(() => mediums.value.some(medium => medium.name === 'video'));
|
||||||
|
const hasSnapshot = computed(() => mediums.value.some(medium => medium.name === 'snapshot'));
|
||||||
|
|
||||||
|
// 获取视频和图片的文件
|
||||||
|
const videoFile = computed(() => {
|
||||||
|
const video = mediums.value.find(medium => medium.name === 'video');
|
||||||
|
return video ? video.file : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshotFile = computed(() => {
|
||||||
|
const snapshot = mediums.value.find(medium => medium.name === 'snapshot');
|
||||||
|
return snapshot ? snapshot.file : '';
|
||||||
|
});
|
||||||
|
|
||||||
// 获取类型映射
|
// 获取类型映射
|
||||||
const fetchTypeMapping = async (token) => {
|
const fetchTypeMapping = async (token) => {
|
||||||
|
@ -146,11 +193,32 @@ const fetchEvents = async () => {
|
||||||
const { tableData: data, totalItems: total } = await boxApi.getEvents(token.value, pageSize.value, currentPage.value); // 使用 BoxApi 的 getEvents 方法
|
const { tableData: data, totalItems: total } = await boxApi.getEvents(token.value, pageSize.value, currentPage.value); // 使用 BoxApi 的 getEvents 方法
|
||||||
tableData.value = data;
|
tableData.value = data;
|
||||||
totalItems.value = total;
|
totalItems.value = total;
|
||||||
|
animateNumberChange(total);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching events data:", error);
|
console.error("Error fetching events data:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const animateNumberChange = (newTotal) => {
|
||||||
|
const stepTime = 50; // 每次递增的时间间隔
|
||||||
|
// const stepCount = Math.abs(newTotal - displayTotalItems.value);
|
||||||
|
const stepCount = Math.abs(newTotal - displayTotalItems.value) / 20;
|
||||||
|
|
||||||
|
const incrementStep = Math.ceil((newTotal - displayTotalItems.value) / stepCount); // 每次递增的值
|
||||||
|
|
||||||
|
const step = () => {
|
||||||
|
if (displayTotalItems.value < newTotal) {
|
||||||
|
displayTotalItems.value = Math.min(displayTotalItems.value + incrementStep, newTotal);
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
} else if (displayTotalItems.value > newTotal) {
|
||||||
|
displayTotalItems.value = Math.max(displayTotalItems.value - incrementStep, newTotal);
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
step();
|
||||||
|
};
|
||||||
|
|
||||||
// 查看告警详情
|
// 查看告警详情
|
||||||
const handleView = (row) => {
|
const handleView = (row) => {
|
||||||
selectedRow.value = row;
|
selectedRow.value = row;
|
||||||
|
@ -209,7 +277,7 @@ onMounted(async () => {
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-pan {
|
/* .top-pan {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -223,7 +291,7 @@ onMounted(async () => {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-shadow: 0 20px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 20px 8px rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.middle-row {
|
.middle-row {
|
||||||
min-height: 5vw;
|
min-height: 5vw;
|
||||||
|
@ -233,12 +301,12 @@ onMounted(async () => {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background: linear-gradient(to bottom left, rgb(150, 151, 243), rgb(215, 214, 250));
|
background: linear-gradient(to bottom left, rgb(150, 151, 243), rgb(215, 214, 250));
|
||||||
border-radius: 8px;
|
/* border-radius: 8px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 380px;
|
height: 620px;
|
||||||
min-height: 60%;
|
min-height: 60%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
@ -257,6 +325,11 @@ onMounted(async () => {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-media {
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-container {
|
.media-container {
|
||||||
|
|
|
@ -1,317 +1,92 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="alert-container">
|
<div class="alert-container">
|
||||||
<el-row class="top-pan">
|
<el-row class="bottom-pan">
|
||||||
</el-row>
|
<el-col :span="24" class="panel-bottom">
|
||||||
<el-row class="middle-row">
|
<Cameras/>
|
||||||
<span>
|
</el-col>
|
||||||
告警总数:{{ displayTotalItems }}
|
</el-row>
|
||||||
</span>
|
|
||||||
</el-row>
|
<el-row class="top-pan">
|
||||||
<el-row class="table-row">
|
<el-col :sm="24" :md="12" class="panel-top-left">
|
||||||
<el-col :span="24">
|
<statistics />
|
||||||
<div class="table-container">
|
<!-- <div class="placeholder">预留块区域</div> -->
|
||||||
<el-table :data="tableData" style="width:100%; height: 100%;" header-row-class-name="table-header"
|
</el-col>
|
||||||
:fit="true">
|
<el-col :sm="24" :md="12" class="panel-top-right">
|
||||||
<el-table-column prop="id" label="告警编号" min-width="100"></el-table-column>
|
<alertChart />
|
||||||
<el-table-column label="告警类型" min-width="150">
|
</el-col>
|
||||||
<template v-slot="scope">
|
</el-row>
|
||||||
{{ typeMapping[scope.row.types] }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
</div>
|
||||||
<el-table-column prop="camera.name" label="告警位置" min-width="150"></el-table-column>
|
|
||||||
<el-table-column label="告警时间" min-width="200">
|
|
||||||
<template v-slot="scope">
|
|
||||||
{{ formatDateTime(scope.row.ended_at) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="status" label="告警状态" min-width="100">
|
|
||||||
<template v-slot="scope">
|
|
||||||
<el-tag :type="scope.row.status === 'pending' ? 'warning' : 'success'">{{
|
|
||||||
statusMapping[scope.row.status]
|
|
||||||
}}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" min-width="100">
|
|
||||||
<template v-slot="scope">
|
|
||||||
<el-button type="text" @click="handleView(scope.row)">查看</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
<div class="pagination-container">
|
|
||||||
<el-pagination @size-change="handleSizeChange" @current-change="handlePageChange"
|
|
||||||
:current-page="currentPage" :page-size="pageSize" :total="totalItems"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper">
|
|
||||||
</el-pagination>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-dialog v-model="dialogVisible" title="告警详情" width="80%">
|
|
||||||
<div>
|
|
||||||
<el-row>
|
|
||||||
<el-col :span="6">
|
|
||||||
<p>告警编号: {{ selectedRow.id }}</p>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<p>告警类型: {{ typeMapping[selectedRow.types] }}</p>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<p>告警位置: {{ selectedRow.camera.name }}</p>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<p>告警时间: {{ selectedRow.formatted_started_at }}</p>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-row>
|
|
||||||
<el-col :span="6">
|
|
||||||
<p>告警状态: <el-tag :type="selectedRow.status === 'pending' ? 'warning' : 'success'">{{
|
|
||||||
statusMapping[selectedRow.status]
|
|
||||||
}}</el-tag></p>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<p>摄像头编号: {{ selectedRow.camera_id }}</p>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<p>持续时间: {{ duration }}</p>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="6">
|
|
||||||
<p>备注: {{ selectedRow.note }}</p>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<div class="event-media">
|
|
||||||
<div v-for="medium in selectedRow.mediums" :key="medium.id" class="media-container">
|
|
||||||
<div v-if="medium.name === 'video'" class="media-item video-item">
|
|
||||||
<p>告警关联视频</p>
|
|
||||||
<video :src="medium.file" controls></video>
|
|
||||||
</div>
|
|
||||||
<div v-if="medium.name === 'snapshot'" class="media-item image-item">
|
|
||||||
<p>告警关联图片</p>
|
|
||||||
<el-image :src="medium.file" fit="contain"></el-image>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span slot="footer" class="dialog-footer">
|
|
||||||
<div class=""></div>
|
|
||||||
</span>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue';
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import Statistics from '@/components/Statistics.vue';
|
||||||
|
import AlertChart from '@/components/AlertChart.vue';
|
||||||
|
import Cameras from '@/components/Cameras.vue';
|
||||||
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
||||||
|
|
||||||
|
|
||||||
// 创建 BoxApi 实例
|
// 创建 BoxApi 实例
|
||||||
const boxApi = new BoxApi();
|
const boxApi = new BoxApi();
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const tableData = ref([]);
|
|
||||||
const dialogVisible = ref(false);
|
|
||||||
const selectedRow = ref({});
|
|
||||||
const mediums = ref([]);
|
|
||||||
const duration = ref('');
|
|
||||||
const typeMapping = reactive({});
|
const typeMapping = reactive({});
|
||||||
const statusMapping = {
|
|
||||||
'pending': '待处理',
|
|
||||||
'closed': '已处理'
|
|
||||||
};
|
|
||||||
const currentPage = ref(1);
|
|
||||||
const pageSize = ref(20);
|
|
||||||
const token = ref(null);
|
const token = ref(null);
|
||||||
const totalItems = ref(0);
|
|
||||||
const displayTotalItems = ref(0); // 用于展示的数字
|
|
||||||
|
|
||||||
// 获取类型映射
|
// 获取类型映射
|
||||||
const fetchTypeMapping = async (token) => {
|
const fetchTypeMapping = async (token) => {
|
||||||
try {
|
try {
|
||||||
const algorithms = await boxApi.getAlgorithms(token); // 使用 BoxApi 的 getAlgorithms 方法
|
const algorithms = await boxApi.getAlgorithms(token); // 使用 BoxApi 的 getAlgorithms 方法
|
||||||
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
||||||
algorithms.forEach((algorithm) => {
|
algorithms.forEach((algorithm) => {
|
||||||
typeMapping[algorithm.code_name] = algorithm.name;
|
typeMapping[algorithm.code_name] = algorithm.name;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching algorithms:", error);
|
console.error("Error fetching algorithms:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取告警数据
|
|
||||||
const fetchEvents = async () => {
|
|
||||||
try {
|
|
||||||
const { tableData: data, totalItems: total } = await boxApi.getEvents(token.value, pageSize.value, currentPage.value);
|
|
||||||
tableData.value = data;
|
|
||||||
totalItems.value = total;
|
|
||||||
animateNumberChange(total);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching events data:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const animateNumberChange = (newTotal) => {
|
|
||||||
const stepTime = 50; // 每次递增的时间间隔
|
|
||||||
// const stepCount = Math.abs(newTotal - displayTotalItems.value);
|
|
||||||
const stepCount = Math.abs(newTotal - displayTotalItems.value)/20;
|
|
||||||
|
|
||||||
const incrementStep = Math.ceil((newTotal - displayTotalItems.value) / stepCount); // 每次递增的值
|
|
||||||
|
|
||||||
const step = () => {
|
|
||||||
if (displayTotalItems.value < newTotal) {
|
|
||||||
displayTotalItems.value = Math.min(displayTotalItems.value + incrementStep, newTotal);
|
|
||||||
requestAnimationFrame(step);
|
|
||||||
} else if (displayTotalItems.value > newTotal) {
|
|
||||||
displayTotalItems.value = Math.max(displayTotalItems.value - incrementStep, newTotal);
|
|
||||||
requestAnimationFrame(step);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
step();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 查看告警详情
|
|
||||||
const handleView = (row) => {
|
|
||||||
selectedRow.value = row;
|
|
||||||
duration.value = calculateDuration(row.started_at, row.ended_at);
|
|
||||||
row.formatted_started_at = formatDateTime(row.started_at);
|
|
||||||
dialogVisible.value = true;
|
|
||||||
mediums.value = row.mediums || [];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 分页处理
|
|
||||||
const handlePageChange = (page) => {
|
|
||||||
currentPage.value = page;
|
|
||||||
fetchEvents();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页大小处理
|
|
||||||
const handleSizeChange = (size) => {
|
|
||||||
pageSize.value = size;
|
|
||||||
fetchEvents();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 计算持续时间
|
|
||||||
const calculateDuration = (started_at, ended_at) => {
|
|
||||||
const start = new Date(started_at);
|
|
||||||
const end = new Date(ended_at);
|
|
||||||
const durationMs = end - start;
|
|
||||||
const minutes = Math.floor(durationMs / 60000);
|
|
||||||
const seconds = ((durationMs % 60000) / 1000).toFixed(0);
|
|
||||||
return `${minutes}分${seconds < 10 ? '0' : ''}${seconds}秒`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 格式化日期时间
|
|
||||||
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}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载后执行
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
token.value = localStorage.getItem('alertToken');
|
token.value = localStorage.getItem('alertToken');
|
||||||
await fetchTypeMapping(token.value);
|
await fetchTypeMapping(token.value);
|
||||||
await fetchEvents();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.alert-container {
|
.alert-container {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .top-pan {
|
.top-pan {
|
||||||
padding: 0px;
|
/* padding: 10px; */
|
||||||
margin: 0px;
|
/* margin-bottom: 10px; */
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
} */
|
height: 55vh;
|
||||||
|
}
|
||||||
/* .panel-section {
|
.bottom-pan{
|
||||||
flex: 1;
|
margin: 0;
|
||||||
background-color: #fff;
|
padding: 0;
|
||||||
box-shadow: 0 20px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
border-radius: 15px;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.middle-row {
|
|
||||||
min-height: 5vw;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-content: center;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: bold;
|
|
||||||
background: linear-gradient(to bottom left, rgb(150, 151, 243), rgb(215, 214, 250));
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.panel-top-left {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
height: 380px;
|
display: flex;
|
||||||
min-height: 60%;
|
flex-direction: column;
|
||||||
overflow-x: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-header {
|
.panel-top-right {
|
||||||
background-color: #f7f8fa;
|
flex: 1;
|
||||||
font-weight: bold;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-table th.el-table__cell {
|
.panel-bottom{
|
||||||
background-color: #000;
|
margin: 0;
|
||||||
color: #fff;
|
padding: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.event-media {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-container {
|
|
||||||
flex: 0 0 48%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-item video,
|
|
||||||
.image-item img {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-item p,
|
|
||||||
.image-item p {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
background-color: #e8e9e4;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .pagination-container .el-pagination__total,
|
|
||||||
::v-deep .pagination-container .el-pagination__goto,
|
|
||||||
::v-deep .pagination-container .el-pagination__classifier {
|
|
||||||
color: #000;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,12 +1,355 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="alert-container">
|
||||||
nihao
|
<el-row class="top-pan">
|
||||||
|
<el-col :sm="24" :md="12" class="panel-section">
|
||||||
|
<statistics />
|
||||||
|
</el-col>
|
||||||
|
<el-col :sm="24" :md="12" class="panel-section">
|
||||||
|
<alertChart />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row class="middle-row">
|
||||||
|
<span>
|
||||||
|
告警总数:{{ displayTotalItems }}
|
||||||
|
</span>
|
||||||
|
</el-row>
|
||||||
|
<el-row class="table-row">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div class="table-container">
|
||||||
|
<el-table :data="tableData" style="width:100%; height: 100%;" header-row-class-name="table-header" :fit="true">
|
||||||
|
<el-table-column prop="id" label="告警编号" min-width="100"></el-table-column>
|
||||||
|
<el-table-column label="告警类型" min-width="150">
|
||||||
|
<template v-slot="scope">
|
||||||
|
{{ typeMapping[scope.row.types] }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="camera.name" label="告警位置" min-width="150"></el-table-column>
|
||||||
|
<el-table-column label="告警时间" min-width="200">
|
||||||
|
<template v-slot="scope">
|
||||||
|
{{ formatDateTime(scope.row.ended_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="告警状态" min-width="100">
|
||||||
|
<template v-slot="scope">
|
||||||
|
<el-tag :type="scope.row.status === 'pending' ? 'warning' : 'success'">{{ statusMapping[scope.row.status]
|
||||||
|
}}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" min-width="100">
|
||||||
|
<template v-slot="scope">
|
||||||
|
<el-button type="text" @click="handleView(scope.row)">查看</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination @size-change="handleSizeChange" @current-change="handlePageChange" :current-page="currentPage"
|
||||||
|
:page-size="pageSize" :total="totalItems" layout="total, sizes, prev, pager, next, jumper">
|
||||||
|
</el-pagination>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-dialog v-model="dialogVisible" title="告警详情" width="80%">
|
||||||
|
<div>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="6">
|
||||||
|
<p>告警编号: {{ selectedRow.id }}</p>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<p>告警类型: {{ typeMapping[selectedRow.types] }}</p>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<p>告警位置: {{ selectedRow.camera.name }}</p>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<p>告警时间: {{ selectedRow.formatted_started_at }}</p>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="6">
|
||||||
|
<p>告警状态: <el-tag :type="selectedRow.status === 'pending' ? 'warning' : 'success'">{{
|
||||||
|
statusMapping[selectedRow.status]
|
||||||
|
}}</el-tag></p>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<p>摄像头编号: {{ selectedRow.camera_id }}</p>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<p>持续时间: {{ duration }}</p>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<p>备注: {{ selectedRow.note }}</p>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<!-- <div class="event-media">
|
||||||
|
<div v-for="medium in selectedRow.mediums" :key="medium.id" class="media-container">
|
||||||
|
<div v-if="medium.name === 'video'" class="media-item video-item">
|
||||||
|
<p>告警关联视频</p>
|
||||||
|
<video :src="medium.file" controls></video>
|
||||||
|
</div>
|
||||||
|
<div v-if="medium.name === 'snapshot'" class="media-item image-item">
|
||||||
|
<p>告警关联图片</p>
|
||||||
|
<el-image :src="medium.file" fit="contain"></el-image>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
<div class="event-media" :class="{ 'center-media': mediums.length === 1 }">
|
||||||
|
<!-- 告警关联视频 -->
|
||||||
|
<div v-if="hasVideo" class="media-container">
|
||||||
|
<p>告警关联视频</p>
|
||||||
|
<video :src="videoFile" controls></video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 告警关联图片 -->
|
||||||
|
<div v-if="hasSnapshot" class="media-container">
|
||||||
|
<p>告警关联图片</p>
|
||||||
|
<el-image :src="snapshotFile" fit="contain"></el-image>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<div class=""></div>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue';
|
||||||
</script>
|
import Statistics from '@/components/Statistics.vue';
|
||||||
<style scoped>
|
import AlertChart from '@/components/AlertChart.vue';
|
||||||
|
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
||||||
</style>
|
|
||||||
|
// 创建 BoxApi 实例
|
||||||
|
const boxApi = new BoxApi();
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const tableData = ref([]);
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const selectedRow = ref({});
|
||||||
|
const mediums = ref([]);
|
||||||
|
const duration = ref('');
|
||||||
|
const typeMapping = reactive({});
|
||||||
|
const statusMapping = {
|
||||||
|
'pending': '待处理',
|
||||||
|
'closed': '已处理'
|
||||||
|
};
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(20);
|
||||||
|
const token = ref(null);
|
||||||
|
const totalItems = ref(0);
|
||||||
|
const displayTotalItems = ref(0); // 用于展示的数字
|
||||||
|
|
||||||
|
// 计算属性:根据 mediums 动态判断是否存在视频和图片
|
||||||
|
const hasVideo = computed(() => mediums.value.some(medium => medium.name === 'video'));
|
||||||
|
const hasSnapshot = computed(() => mediums.value.some(medium => medium.name === 'snapshot'));
|
||||||
|
|
||||||
|
// 获取视频和图片的文件
|
||||||
|
const videoFile = computed(() => {
|
||||||
|
const video = mediums.value.find(medium => medium.name === 'video');
|
||||||
|
return video ? video.file : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshotFile = computed(() => {
|
||||||
|
const snapshot = mediums.value.find(medium => medium.name === 'snapshot');
|
||||||
|
return snapshot ? snapshot.file : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取类型映射
|
||||||
|
const fetchTypeMapping = async (token) => {
|
||||||
|
try {
|
||||||
|
const algorithms = await boxApi.getAlgorithms(token); // 使用 BoxApi 的 getAlgorithms 方法
|
||||||
|
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
||||||
|
algorithms.forEach((algorithm) => {
|
||||||
|
typeMapping[algorithm.code_name] = algorithm.name;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching algorithms:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取告警数据
|
||||||
|
const fetchEvents = async () => {
|
||||||
|
try {
|
||||||
|
const { tableData: data, totalItems: total } = await boxApi.getEvents(token.value, pageSize.value, currentPage.value); // 使用 BoxApi 的 getEvents 方法
|
||||||
|
tableData.value = data;
|
||||||
|
totalItems.value = total;
|
||||||
|
animateNumberChange(total);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching events data:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const animateNumberChange = (newTotal) => {
|
||||||
|
const stepTime = 50; // 每次递增的时间间隔
|
||||||
|
// const stepCount = Math.abs(newTotal - displayTotalItems.value);
|
||||||
|
const stepCount = Math.abs(newTotal - displayTotalItems.value) / 20;
|
||||||
|
|
||||||
|
const incrementStep = Math.ceil((newTotal - displayTotalItems.value) / stepCount); // 每次递增的值
|
||||||
|
|
||||||
|
const step = () => {
|
||||||
|
if (displayTotalItems.value < newTotal) {
|
||||||
|
displayTotalItems.value = Math.min(displayTotalItems.value + incrementStep, newTotal);
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
} else if (displayTotalItems.value > newTotal) {
|
||||||
|
displayTotalItems.value = Math.max(displayTotalItems.value - incrementStep, newTotal);
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
step();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看告警详情
|
||||||
|
const handleView = (row) => {
|
||||||
|
selectedRow.value = row;
|
||||||
|
duration.value = calculateDuration(row.started_at, row.ended_at);
|
||||||
|
row.formatted_started_at = formatDateTime(row.started_at);
|
||||||
|
dialogVisible.value = true;
|
||||||
|
mediums.value = row.mediums || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
currentPage.value = page;
|
||||||
|
fetchEvents();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页大小处理
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pageSize.value = size;
|
||||||
|
fetchEvents();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算持续时间
|
||||||
|
const calculateDuration = (started_at, ended_at) => {
|
||||||
|
const start = new Date(started_at);
|
||||||
|
const end = new Date(ended_at);
|
||||||
|
const durationMs = end - start;
|
||||||
|
const minutes = Math.floor(durationMs / 60000);
|
||||||
|
const seconds = ((durationMs % 60000) / 1000).toFixed(0);
|
||||||
|
return `${minutes}分${seconds < 10 ? '0' : ''}${seconds}秒`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
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}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面加载后执行
|
||||||
|
onMounted(async () => {
|
||||||
|
token.value = localStorage.getItem('alertToken');
|
||||||
|
await fetchTypeMapping(token.value);
|
||||||
|
await fetchEvents();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.alert-container {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-pan {
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-section {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 20px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 15px;
|
||||||
|
transform: scale(0.8);
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle-row {
|
||||||
|
min-height: 5vw;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: linear-gradient(to bottom left, rgb(150, 151, 243), rgb(215, 214, 250));
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 650px;
|
||||||
|
min-height: 60%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-table th.el-table__cell {
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-media {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-media {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-container {
|
||||||
|
flex: 0 0 48%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item video,
|
||||||
|
.image-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item p,
|
||||||
|
.image-item p {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
background-color: #e8e9e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .pagination-container .el-pagination__total,
|
||||||
|
::v-deep .pagination-container .el-pagination__goto,
|
||||||
|
::v-deep .pagination-container .el-pagination__classifier {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Login from '@/html/LoginView.vue';
|
||||||
import UserList from '@/html/UserList.vue';
|
import UserList from '@/html/UserList.vue';
|
||||||
import Home from '@/html/Home.vue';
|
import Home from '@/html/Home.vue';
|
||||||
import DataStatistics from '@/html/DataStatistics.vue';
|
import DataStatistics from '@/html/DataStatistics.vue';
|
||||||
|
import Cameras from '@/components/Cameras.vue';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
@ -58,6 +59,12 @@ const routes = [
|
||||||
name: 'DataStatistics',
|
name: 'DataStatistics',
|
||||||
component: DataStatistics,
|
component: DataStatistics,
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path:'/cameras',
|
||||||
|
name: 'Cameras',
|
||||||
|
component: Cameras,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue