431 lines
13 KiB
Vue
431 lines
13 KiB
Vue
<template>
|
||
<div class="alert-container">
|
||
<div class="title-count">
|
||
<!-- <el-row class="title-row">
|
||
今日事件列表
|
||
</el-row> -->
|
||
<el-row class="total-row">
|
||
<div class="total-text">
|
||
今日告警
|
||
</div>
|
||
<div class="total-text">
|
||
{{ totalItems }}条
|
||
</div>
|
||
</el-row>
|
||
</div>
|
||
|
||
<el-row class="table-row">
|
||
<el-col :span="24" class="table-col">
|
||
<div class="table-container">
|
||
<el-table :data="tableData" @row-click="handleRowClick" class="table-part">
|
||
<el-table-column v-show="false" prop="id" label="告警编号" v-if="false"></el-table-column>
|
||
<el-table-column label="告警类型" :width="adjustedWidths[0]" align="center"
|
||
:show-overflow-tooltip="true">
|
||
<template v-slot="scope">
|
||
{{ typeMapping[scope.row.types] }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="camera.name" label="告警位置" :width="adjustedWidths[1]" align="center"
|
||
:show-overflow-tooltip="true"></el-table-column>
|
||
<el-table-column label="告警时间" :width="adjustedWidths[2]" align="center"
|
||
:show-overflow-tooltip="true">
|
||
<template v-slot="scope">
|
||
{{ formatDateTime(scope.row.ended_at) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="status" label="告警状态" v-if="false">
|
||
<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>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- <el-row class="table-row">
|
||
<el-col :span="24" class="table-col">
|
||
<div class="table-container">
|
||
<dv-scroll-board :config="tableConfig" class="table-part" @row-click="handleRowClick"></dv-scroll-board>
|
||
</div>
|
||
</el-col>
|
||
</el-row> -->
|
||
|
||
<!-- <div class="adjusted-widths-display">
|
||
当前列宽: {{ adjustedWidths.join(', ') }}
|
||
</div> -->
|
||
|
||
<el-dialog v-model="dialogVisible" title="告警详情" width="50%" class="dialog-container">
|
||
<div>
|
||
<el-row class="dialog-row">
|
||
<!-- 左侧告警图片和图片信息 -->
|
||
<el-col :span="12" class="dialog-left">
|
||
<el-row gutter class="dialog-image-container">
|
||
<!-- 根据 mediumType 显示视频或图片,确保只显示一种或两种 -->
|
||
<template v-if="hasSnapshot">
|
||
<el-image :src="snapshotFile"></el-image>
|
||
</template>
|
||
<template v-if="hasVideo">
|
||
<video :src="videoFile" controls></video>
|
||
</template>
|
||
</el-row>
|
||
</el-col>
|
||
|
||
<!-- 右侧告警信息 -->
|
||
<el-col :span="11" class="dialog-right">
|
||
<el-row>
|
||
<el-col :span="24">
|
||
<p>告警编号: {{ selectedRow.id }}</p>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<p>告警类型: {{ typeMapping[selectedRow.types] }}</p>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<p>告警位置: {{ selectedRow.camera.name }}</p>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<p>告警时间: {{ selectedRow.formatted_started_at }}</p>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<p>告警状态: <el-tag :type="selectedRow.status === 'pending' ? 'warning' : 'success'">{{
|
||
statusMapping[selectedRow.status]
|
||
}}</el-tag></p>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<p>摄像头编号: {{ selectedRow.camera_id }}</p>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<p>持续时间: {{ duration }}</p>
|
||
</el-col>
|
||
<!-- <el-col :span="24">
|
||
<p>备注: {{ selectedRow.remark }}</p>
|
||
</el-col> -->
|
||
|
||
<!-- <el-col :span="24">
|
||
<el-form-item style="width: 90%;">
|
||
<el-input v-model="remark" placeholder="请描述事件原因" type="textarea" rows="5" :disabled="true"></el-input>
|
||
</el-form-item>
|
||
</el-col> -->
|
||
</el-row>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||
import { BoxApi } from '@/utils/boxApi.ts';
|
||
|
||
const boxApi = new BoxApi();
|
||
const tableData = ref([]);
|
||
const dialogVisible = ref(false);
|
||
const remark = ref("");
|
||
const selectedRow = ref({});
|
||
const typeMapping = reactive({});
|
||
const currentPage = ref(1);
|
||
const pageSize = ref(20);
|
||
const totalItems = ref(0);
|
||
const displayTotalItems = ref(0);
|
||
const token = ref(null);
|
||
const apiInstance = new BoxApi();
|
||
const statusMapping = {
|
||
'pending': '待处理',
|
||
'assigned': '处理中',
|
||
'closed': '已处理'
|
||
};
|
||
const duration = ref('');
|
||
|
||
const hasSnapshot = ref(false);
|
||
const hasVideo = ref(false);
|
||
const snapshotFile = ref("");
|
||
const videoFile = ref("");
|
||
const originalWidths = [97, 150, 160]; // 默认宽度
|
||
const adjustedWidths = ref([...originalWidths]);
|
||
const baseWidth = 2150;
|
||
|
||
const adjustColumnWidths = () => {
|
||
const currentWidth = window.innerWidth;
|
||
// console.log(">>>>>>>>>>", currentWidth);
|
||
const scale = currentWidth / baseWidth;
|
||
// console.log(">>>>>>>>>>", scale);
|
||
|
||
// const adjustedScale = Math.max(scale, 0.5);
|
||
adjustedWidths.value = originalWidths.map(width => {
|
||
return currentWidth < baseWidth
|
||
? width * scale // 缩小
|
||
: width * scale; // 放大
|
||
});
|
||
// nextTick(() => {
|
||
|
||
// });
|
||
};
|
||
|
||
// const tableConfig = ref({
|
||
// header: ['告警类型', '告警位置', '告警时间'],
|
||
// data: [],
|
||
// rowNum: 5,
|
||
// columnWidth: [100, 160, 190],
|
||
// carousel: 'single',
|
||
// });
|
||
|
||
const fetchTypeMapping = async (token) => {
|
||
try {
|
||
const algorithms = await boxApi.getAlgorithms(token);
|
||
// console.log("Algorithms:", algorithms);
|
||
algorithms.forEach((algorithm) => {
|
||
typeMapping[algorithm.code_name] = algorithm.name;
|
||
});
|
||
} catch (error) {
|
||
console.error("Error fetching algorithms:", error);
|
||
}
|
||
};
|
||
|
||
const fetchEvents = async () => {
|
||
try {
|
||
const token = localStorage.getItem('alertToken');
|
||
const limit = 1000;
|
||
let allEvents = [];
|
||
let currentPage = 1;
|
||
|
||
const { endOfToday, startOfToday } = getDateParams();
|
||
|
||
const timeBefore = formatDateTime(endOfToday);
|
||
const timeAfter = formatDateTime(startOfToday);
|
||
// console.log("Start of today:", timeAfter);
|
||
// const firstResponse = await apiInstance.getEventsByParams(token, limit, currentPage);
|
||
const firstResponse = await apiInstance.getEventsByParams(token, limit, currentPage,timeBefore, timeAfter);
|
||
// console.log("Total items:", firstResponse);
|
||
|
||
const total = firstResponse.count;
|
||
totalItems.value = total;
|
||
|
||
allEvents = firstResponse.results;
|
||
|
||
while (allEvents.length < total) {
|
||
currentPage += 1;
|
||
const response = await apiInstance.getEventsByParams(token, limit, currentPage);
|
||
allEvents = allEvents.concat(response.results);
|
||
}
|
||
tableData.value = allEvents;
|
||
// tableConfig.value.data = allEvents.map(event => [
|
||
// typeMapping[event.types],
|
||
// event.camera.name,
|
||
// formatDateTime(event.ended_at)
|
||
// ]);
|
||
// console.log(">>>>>>>>>>events IDs:", allEvents.map(event => event.id));
|
||
} catch (error) {
|
||
console.error("Error fetching events data:", error);
|
||
}
|
||
};
|
||
|
||
const getDateParams = () => {
|
||
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 {
|
||
startOfToday,
|
||
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 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 handleRowClick = (row) => {
|
||
selectedRow.value = row;
|
||
duration.value = calculateDuration(row.started_at, row.ended_at);
|
||
row.formatted_started_at = formatDateTime(row.started_at);
|
||
|
||
// 重置媒体类型
|
||
hasSnapshot.value = false;
|
||
hasVideo.value = false;
|
||
snapshotFile.value = "";
|
||
videoFile.value = "";
|
||
|
||
// 判断并设置媒体文件路径
|
||
row.mediums.forEach((medium) => {
|
||
if (medium.name === "snapshot") {
|
||
hasSnapshot.value = true;
|
||
snapshotFile.value = medium.file;
|
||
} else if (medium.name === "video") {
|
||
hasVideo.value = true;
|
||
videoFile.value = medium.file;
|
||
}
|
||
});
|
||
dialogVisible.value = true;
|
||
};
|
||
|
||
|
||
const handlePageChange = (page) => {
|
||
currentPage.value = page;
|
||
fetchEvents();
|
||
};
|
||
|
||
// const calculateColumnWidths = () => {
|
||
// const containerWidth = document.querySelector('.table-container').clientWidth;
|
||
// };
|
||
|
||
|
||
onMounted(async () => {
|
||
token.value = localStorage.getItem('alertToken');
|
||
await fetchTypeMapping(token.value);
|
||
await fetchEvents();
|
||
await adjustColumnWidths();
|
||
window.addEventListener('resize', adjustColumnWidths);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener('resize', adjustColumnWidths);
|
||
});
|
||
</script>
|
||
|
||
|
||
<style scoped>
|
||
.alert-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 90%;
|
||
height: 100%;
|
||
margin: 0 0 0 1vw;
|
||
box-sizing: border-box;
|
||
background-color: transparent;
|
||
}
|
||
|
||
.table-continer {
|
||
width: 100%;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.table-row:hover {
|
||
cursor: pointer;
|
||
}
|
||
|
||
|
||
.title-count {
|
||
width: 80%;
|
||
text-align: center;
|
||
|
||
/* background-color: transparent; */
|
||
}
|
||
|
||
|
||
/* .total-text{
|
||
padding-right: 1vw;
|
||
} */
|
||
/* title-row内容水平上下居中 */
|
||
.title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
}
|
||
|
||
/* total-row左右等分,分别靠左和靠右 */
|
||
.total-row {
|
||
display: flex;
|
||
/* justify-content: space-between; */
|
||
justify-content: right;
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
padding-right: 0vw;
|
||
}
|
||
|
||
.dialog-row {
|
||
gap: 30px;
|
||
display: flex;
|
||
text-align: left;
|
||
flex-direction: row;
|
||
}
|
||
|
||
.dialog-image-container {
|
||
gap: 20px;
|
||
}
|
||
|
||
.el-dialog .el-image,
|
||
.el-dialog video {
|
||
width: 100%;
|
||
}
|
||
|
||
|
||
/* 基础表格样式 */
|
||
::v-deep .el-table {
|
||
color: white;
|
||
font-size: 13px;
|
||
/* font-weight: bold; */
|
||
background-color: transparent;
|
||
padding: 0;
|
||
margin: 0;
|
||
height: 14vh;
|
||
}
|
||
|
||
/* 表头和单元格基本样式 */
|
||
|
||
::v-deep .el-table td {
|
||
border: none;
|
||
background-color: #001529;
|
||
transition: border 0.3s ease, background-color 0.3s ease;
|
||
color: white;
|
||
padding: 0;
|
||
/* height: 10px; */
|
||
/* white-space: nowrap; */
|
||
/* overflow: hidden; */
|
||
/* text-overflow: ellipsis; */
|
||
}
|
||
|
||
/* 去掉中间数据的分割线 */
|
||
/* ::v-deep .el-table .el-table__row>td{
|
||
border: none;
|
||
} */
|
||
|
||
.table-container >>> .el-table__row>td{
|
||
border: none;
|
||
}
|
||
.table-container >>> .el-table th.is-leaf{
|
||
border: none;
|
||
}
|
||
::v-deep .el-table__inner-wrapper::before{
|
||
height: 0;
|
||
}
|
||
|
||
::v-deep .el-table thead th {
|
||
color: white;
|
||
font-weight: bold;
|
||
background-color: #001529;
|
||
padding: 0;
|
||
}
|
||
|
||
::v-deep .el-table .el-table__cell:hover {
|
||
background-color: transparent;
|
||
color: rgb(238, 150, 49);
|
||
/* font-size: 26px; */
|
||
/* 保持文本为白色 */
|
||
}
|
||
|
||
.el-tooltip__popper {
|
||
font-size: 26px;
|
||
max-width:50%
|
||
}
|
||
</style>
|
||
|