Files
local_alert/src/html/AlertManagement.vue

651 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="alert-container">
<!-- <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="filter-row">
<el-col :span="4">
<el-form-item label="摄像头名称">
<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="3">
<el-form-item label="告警类型">
<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="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="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="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>
<el-row class="table-row">
<el-col :span="24" class="table-col">
<div class="table-container">
<el-table :data="tableData" header-row-class-name="table-header" :fit="true" height="580">
<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="150">
<template v-slot="scope">
<el-button type="text" @click="handleView(scope.row)">查看</el-button>
<el-button type="text" v-if="scope.row.status === 'pending'"
@click="submitStatusUpdate('closed', 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.remark }}</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 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>
<!-- 备注输入框 -->
<el-row>
<el-col :span="24">
<el-form-item>
<el-input v-model="remark" placeholder="请描述事件原因,处理过程方法,以及处理结果" type="textarea" rows="5"></el-input>
</el-form-item>
</el-col>
</el-row>
<!-- 条件显示按钮 -->
<el-row class="dialog-button">
<el-col :span="24" style="text-align: center;">
<el-button v-if="selectedRow.status === 'pending'" type="primary" @click="submitStatusUpdate('closed')">
标记为已处理
</el-button>
<el-button v-else type="primary" @click="submitStatusUpdate(selectedRow.status)">
提交
</el-button>
</el-col>
</el-row>
</div>
</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>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
// import Statistics from '@/components/Statistics.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();
// 响应式数据
const tableData = ref([]);
const dialogVisible = ref(false);
const remark = ref("");
const mediaDialogVisible = ref(false);
const mediaType = ref('');
const mediaSrc = ref('');
const selectedRow = ref({});
const mediums = ref([]);
const duration = ref('');
const typeMapping = reactive({});
const statusMapping = {
'pending': '待处理',
'assigned': '处理中',
'closed': '已处理'
};
const currentPage = ref(1);
const pageSize = ref(20);
const token = ref(null);
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');
const limit = 20; // 每次请求 20 条数据
let offset = 0; // 从 0 开始
let allCameras = [];
// 第一次请求,用于获取总数
const firstResponse = await boxApi.getCameras(limit, offset, token);
const cameraCount = firstResponse.count;
allCameras = firstResponse.results;
// 根据总数继续请求剩余的数据
while (offset + limit < cameraCount) {
offset += limit;
const response = await boxApi.getCameras(limit, offset, token);
allCameras = allCameras.concat(response.results);
}
cameras.value = allCameras; // 存储所有摄像头信息
} catch (error) {
console.error("Error fetching cameras:", error);
}
};
const filterParams = reactive({
types: null,
timeAfter: null,
timeBefore: null,
cameraId: null,
status: null,
});
// 更新状态和备注的方法
const submitStatusUpdate = async (newStatus, row = null) => {
try {
// console.console.log(row,row.id);
const eventId = row ? row.id : selectedRow.value.id;
const remarkContent = remark.value && remark.value.trim() !== "" ? remark.value : null;
// 调用 setEventStatus 更新事件状态和备注
await boxApi.setEventStatus(eventId, newStatus, remarkContent);
if (row) {
row.status = newStatus;
row.remark = remarkContent; // 更新该行的备注
} else {
selectedRow.value.status = newStatus;
selectedRow.value.remark = remarkContent;
dialogVisible.value = false; // 关闭弹窗
remark.value = ""; // 清空备注
}
} catch (error) {
console.error("Error updating event status:", error);
}
};
const handleFilter = async () => {
try {
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 ? 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,
pageSize.value,
currentPage.value,
timeBefore,
timeAfter,
types,
cameraId,
status
);
tableData.value = results;
totalItems.value = count;
} catch (error) {
console.error("Error fetching filtered events:", error);
}
};
const handleReset = () => {
filterParams.types = null;
filterParams.timeAfter = null;
filterParams.timeBefore = null;
filterParams.cameraId = null;
filterParams.status = null;
fetchEvents(); // 重置筛选条件后,重新获取所有告警数据
};
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) => {
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 || [];
remark.value = row.remark || "";
};
const closeDialog = () => {
remark.value = "";
};
// 分页处理
const handlePageChange = (page) => {
currentPage.value = page;
if (filterParams.types || filterParams.timeAfter || filterParams.timeBefore || filterParams.cameraId || filterParams.status) {
handleFilter();
} else {
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();
await fetchCameras();
});
</script>
<style scoped>
.alert-container {
padding: 0;
margin: 0;
background-color: #001529;
overflow-y: auto;
height: 100%;
}
/* .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;
} */
/* .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;
} */
.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 {
background-color: #f7f8fa;
font-weight: bold;
}
::v-deep .el-table th.el-table__cell {
background-color: #e9eefca4;
color: #000;
}
.event-media {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
width: 100%;
}
.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;
}
.table-col {
max-height: 100%;
}
.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;
}
::v-deep .pagination-container .el-pagination__total,
::v-deep .pagination-container .el-pagination__goto,
::v-deep .pagination-container .el-pagination__classifier {
color: #000;
}
.dialog-button {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
</style>