<template>
<div class="listManagePage EventAlarmDetail">
<a-breadcrumb separator=">" v-if="route.query.origin == 'violate'">
<img class="dw_img" src="@/assets/images/demoImg/mbx_dw.png" alt="">
<a-breadcrumb-item><a @click="()=>router.push('/roomInOut/personnelViolate')">人员违规行为台账</a></a-breadcrumb-item>
<a-breadcrumb-item>人员违规行为台账详情</a-breadcrumb-item>
</a-breadcrumb>
<a-breadcrumb separator=">" v-else-if="route.query.origin == 'ledger'">
<img class="dw_img" src="@/assets/images/demoImg/mbx_dw.png" alt="">
<a-breadcrumb-item><a @click="()=>router.push('/roomInOut/eventLedger')">事件台账列表</a></a-breadcrumb-item>
<a-breadcrumb-item>事件台账详情</a-breadcrumb-item>
</a-breadcrumb>
<a-breadcrumb separator=">" v-else>
<img class="dw_img" src="@/assets/images/demoImg/mbx_dw.png" alt="">
<a-breadcrumb-item><a @click="()=>router.push('/eventManage/eventAlarm')">AI事件告警处理列表</a></a-breadcrumb-item>
<a-breadcrumb-item>AI事件告警处理事件详情</a-breadcrumb-item>
</a-breadcrumb>
<a-spin :spinning="loading">
<div class="topPart">
<div class="header_right">
<img src="@/assets/images/abnormalEvent.jpg" alt="">
<div>
<h5>{{formModel.deviceName}}-{{formModel.eventName}}事件</h5>
<p><span class="num_header">NO.</span>{{formModel.eventId}}</p>
</div>
</div>
</div>
<div class="bj_white_height">
<div class="tableTitleLine page-header-btn">
<i class="titleIcon"></i>
<span class="title">事件信息</span>
</div>
<a-form ref="topFormRef" class="topForm firstForm" layout="inline" :model="formModel" :label-col="firstLabelCol" :wrapper-col="firstWrapperCol">
<a-row style="width:100%">
<a-col :span="6">
<a-form-item label="事件ID:">
<span>
{{ formModel.eventId }}
</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="事件等级:">
<span>{{ formModel.eventLevel }}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="发生时间:">
<span>{{ formModel.occurrenceTime }}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="事件分类:">
<span>{{ formModel.eventClassification }}</span>
</a-form-item>
</a-col>
</a-row>
<a-row style="width:100%">
<a-col :span="6">
<a-form-item label="事件名称:">
<span>{{ formModel.eventName }}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="事件标题:">
<span>{{ formModel.eventTitle }}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="关联摄像头:">
<span>{{ formModel.deviceName }}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="地市:">
<span>{{ formModel.city }}</span>
</a-form-item>
</a-col>
</a-row>
<a-row style="width:100%">
<a-col :span="6">
<a-form-item label="区县:">
<span>{{ formModel.district }}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="所属机楼:">
<span>{{ formModel.facilityName }}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="所属机房:">
<span>{{ formModel.roomName }}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="所属设备:">
<span>{{ formModel.cameraName }}</span>
</a-form-item>
</a-col>
</a-row>
<!-- <a-row style="width:100%">
<a-col :span="6">
<a-form-item label="事件触发人员名称:">
<span>{{ formModel.triggerPersonName }}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="身份证号:">
<span>{{ formModel.triggerIdNumber }}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="手机号:">
<span>{{ formModel.triggerPhoneNumber }}</span>
</a-form-item>
</a-col>
</a-row> -->
<a-row style="width:100%">
<a-col :span="12">
<a-form-item label="事件图片:">
<div class="image-container" ref="containerRef">
<img
v-if="formModel.eventPicture"
class="img_size"
:src="`/bit-dfs/file/download?fileId=${formModel.eventPicture}&sourceSystem=ULTRA-AIMR`"
@load="onImageLoad"
ref="imgRef"
/>
<canvas ref="canvasRef" class="overlay-canvas"></canvas>
</div>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="事件视频:">
<EventAlarmVideo v-if="formModel.eventVideoFileId || formModel.eventVideo" :formModel="formModel" :deviceId="route.query.deviceRowId"/>
<!-- <video
ref="videoPlayer"
controls
class="img_size"
:src="formModel.eventVideo"
></video> -->
<!-- <img src="https://www.antdv.com/#error"/> -->
</a-form-item>
</a-col>
</a-row>
</a-form>
<div class="tableTitleLine page-header-btn">
<i class="titleIcon"></i>
<span class="title">触发事件人员</span>
</div>
<a-table :dataSource="dataSource" :columns="columns" :pagination="false" :scroll="{ y: 500 }"/>
<div class="tableTitleLine page-header-btn" style="margin-top: 20px;">
<i class="titleIcon"></i>
<span class="title">事件处理</span>
</div>
<a-form ref="formRef" class="topForm secform" layout="vertical" :model="formData" :rules="rules">
<a-row :gutter="24">
<a-col :span="6">
<a-form-item :label="`是否真实告警:`" name="isTrueAlert">
<a-select
v-if="route.query.type!='view'"
v-model:value="formData.isTrueAlert"
placeholder=""
:options="needDispatchData"
>
</a-select>
<span v-else>{{formModel.isTrueAlert}}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :label="`是否清除:`" name="isRecovered">
<a-select
v-if="route.query.type!='view'"
v-model:value="formData.isRecovered"
placeholder=""
:options="isRecoveredData"
>
</a-select>
<span v-else>{{formModel.isRecovered}}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :label="`告警清除时间:`">
<a-date-picker
v-if="route.query.type!='view'"
v-model:value="formData.alertResolvedTime"
show-time
type="date"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择"
style="width: 100%"
:disabled-date="disabledBeforeOccurrenceDate"
:disabled-time="disabledBeforeOccurrenceTime"
:disabled="formData.isRecovered === '否'"
/>
<span v-else>{{formModel.alertResolvedTime}}</span>
</a-form-item>
</a-col>
<!-- <a-col :span="6">
<a-form-item :label="`是否需要派单:`">
<a-select
v-if="route.query.type!='view'"
v-model:value="formData.needDispatch"
placeholder="否"
:options="needDispatchData"
>
</a-select>
<span v-else>{{formModel.needDispatch}}</span>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :label="`派发至:`">
<a-select
v-if="route.query.type!='view'"
v-model:value="formData.dispatchTarget"
placeholder="无"
:options="dispatchTargetData"
>
</a-select>
<span v-else>{{formModel.dispatchTarget}}</span>
</a-form-item>
</a-col> -->
</a-row>
<a-row style="width:100%">
<a-col :span="24">
<a-form-item label="告警说明:" name="alertDescription">
<a-textarea
v-if="route.query.type!='view'"
placeholder="请输入说明内容"
:rows="2"
v-model:value="formData.alertDescription"
/>
<span v-else>{{formModel.alertDescription}}</span>
</a-form-item>
</a-col>
</a-row>
</a-form>
<div class="footer_btn">
<a-button style="margin-right: 20px;" @click="backToList">{{route.query.type!='view'?'取消':'关闭'}}</a-button>
<a-button v-if="route.query.type!='view'" type="primary" @click="confirmData">确认</a-button>
</div>
</div>
</a-spin>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, reactive, watch,watchEffect,computed } from 'vue'
import {useRoute, useRouter} from 'vue-router'
import { updateEventAlarm, queryEventAlarmById } from "@/api/eventManage"
import { message } from 'ant-design-vue'
import moment from 'moment'
import EventAlarmVideo from '@/views/components/EventAlarmVideo.vue'
import axios from 'axios'
const route = useRoute()
const router = useRouter()
const firstLabelCol = ref({ style: { width: '100px' }})
const firstWrapperCol = ref({ span: 14 })
const formModel = ref({
eventId: '',
eventLevel: '',
eventTitle: '',
occurrenceTime: '',
eventClassification: '',
eventName: '',
city: '',
cameraName: '',
district: '',
facilityName: '',
roomName: '',
equipmentName: '',
triggerPersonName: '',
triggerIdNumber: '',
triggerPhoneNumber: '',
eventVideo: '',
eventPicture: '',
deviceName: '',
isTrueAlert: '',
isRecovered: '',
alertResolvedTime: '',
// needDispatch: '',
// dispatchTarget: '',
alertDescription: '',
})
const recognizeCoordinates = ref([])
const formData = ref({
isTrueAlert: '',
isRecovered: '',
alertResolvedTime: '',
// needDispatch: '',
// dispatchTarget: '',
alertDescription: '',
})
const loading = ref(false)
const isRecoveredData = ref([
{
value: '是',
label: '是',
},
{
value: '否',
label: '否',
},
])
const needDispatchData = ref([
{
value: '是',
label: '是',
},
{
value: '否',
label: '否',
},
])
const formRef = ref()
const rules = {
isTrueAlert: [{ required: true, message: '请选择是否真实告警', trigger: 'change' }],
isRecovered: [{ required: true, message: '请选择是否清除', trigger: 'change' }],
// alertResolvedTime: [{ required: true, message: '请选择告警清除时间', trigger: 'change', type: 'object' }],
alertDescription: [{ required: true, message: '请输入告警说明', trigger: 'blur' }],
}
const videoData = ref ({})
const dataSource = ref ([])
const columns = ref([
{
title: '姓名',
dataIndex: 'triggerPersonName',
key: 'triggerPersonName',
},
{
title: '公司',
dataIndex: 'triggerCorporation',
key: 'triggerCorporation',
},
{
title: '手机号',
dataIndex: 'triggerPhoneNumber',
key: 'triggerPhoneNumber',
},
{
title: '身份证号',
dataIndex: 'triggerIdNumber',
key: 'triggerIdNumber',
},
])
onMounted(() => {
fetchData()
})
const fetchData = () => {
loading.value = true
queryEventAlarmById(route.query.id).then(res => {
if(res) {
recognizeCoordinates.value = res.recognizeCoordinates || []
formModel.value = res
dataSource.value = res.triggerPersonInfoList || []
Object.keys(formData.value).forEach(key => {
if (res[key] !== undefined) {
formData.value[key] = res[key]
}
})
// 确保图片加载完成后绘制
nextTick(() => {
if (imgRef.value && imgRef.value.complete) {
onImageLoad()
}
})
}
}).catch((error: any) => {
console.log(error)
}).finally(()=> {
loading.value = false
})
}
const confirmData = () => {
if(!route.query.id) {
return
}
formRef.value.validate().then(() => {
loading.value = true
let data = {
eventId: route.query.id,
...formData.value
}
// console.log(data, 'formData')
updateEventAlarm(data).then((res) => {
message.success('保存成功')
setTimeout(()=>{
backToList()
}, 1000)
}).catch((error: any) => {
console.log(error)
message.error(error.message)
}).finally(()=> {
loading.value = false
})
})
.catch(error => {
console.log('error', error);
})
}
const backToList = () => {
if(route.query.origin == 'violate'){
router.push({ path: '/roomInOut/personnelViolate',query: route.query })
}else if(route.query.origin == 'ledger'){
router.push({ path: '/roomInOut/eventLedger',query: route.query })
}else{
router.push({ path: '/eventManage/eventAlarm',query: route.query })
}
}
// 计算属性:将发生时间转换为 moment 对象
const occurrenceTimeObj = computed(() => {
if (!formModel.value.occurrenceTime) return null;
return moment(formModel.value.occurrenceTime, 'YYYY-MM-DD HH:mm:ss');
});
// 禁用发生时间之前的日期
const disabledBeforeOccurrenceDate = (current) => {
if (!occurrenceTimeObj.value) return false;
// 禁用发生日期之前的所有日期
return current && current < moment(occurrenceTimeObj.value).startOf('day');
};
// 禁用发生时间当天的之前时间
const disabledBeforeOccurrenceTime = () => {
if (!occurrenceTimeObj.value || !formModel.value.alertResolvedTime) {
return {
disabledHours: () => [],
disabledMinutes: () => [],
disabledSeconds: () => []
};
}
const selectedDate = moment(formModel.value.alertResolvedTime);
const occurrenceDate = moment(occurrenceTimeObj.value);
// 如果选择的日期大于发生日期,则不禁用任何时间
if (selectedDate.isAfter(occurrenceDate, 'day')) {
return {
disabledHours: () => [],
disabledMinutes: () => [],
disabledSeconds: () => []
};
}
// 如果选择的日期等于发生日期,则禁用发生时间之前的时间
if (selectedDate.isSame(occurrenceDate, 'day')) {
const occurrenceHour = occurrenceDate.hour();
const occurrenceMinute = occurrenceDate.minute();
const occurrenceSecond = occurrenceDate.second();
return {
disabledHours: () => Array.from({ length: occurrenceHour }, (_, i) => i),
disabledMinutes: (h) => h === occurrenceHour ?
Array.from({ length: occurrenceMinute }, (_, i) => i) : [],
disabledSeconds: (h, m) => h === occurrenceHour && m === occurrenceMinute ?
Array.from({ length: occurrenceSecond }, (_, i) => i) : []
};
}
// 默认不禁用任何时间
return {
disabledHours: () => [],
disabledMinutes: () => [],
disabledSeconds: () => []
};
};
// 监听发生时间变化,自动调整已选的清除时间
watchEffect(() => {
if (
formData.value.alertResolvedTime &&
occurrenceTimeObj.value &&
moment(formData.value.alertResolvedTime).isBefore(occurrenceTimeObj.value)
) {
// 如果已选的清除时间早于发生时间,则重置为发生时间
formData.value.alertResolvedTime = occurrenceTimeObj.value.format('YYYY-MM-DD HH:mm:ss');
}
});
watch(
() => formData.value.isRecovered,
(newValue) => {
// 当值为"否"时,清空告警清除时间
if (newValue === '否') {
formData.value.alertResolvedTime = '';
}
},
{ immediate: true } // 初始化时也执行一次
);
const imgRef = ref(null)
const canvasRef = ref(null)
const containerRef = ref(null)
const onImageLoad = () => {
nextTick(() => {
try {
if (!imgRef.value || !canvasRef.value) return
const img = imgRef.value
const canvas = canvasRef.value
// 确保图片已加载
if (!img.complete) {
img.onload = onImageLoad
return
}
// 设置canvas尺寸与图片相同
canvas.width = img.naturalWidth
canvas.height = img.naturalHeight
const ctx = canvas.getContext('2d')
if (!ctx) return
// 清除之前绘制的内容
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 如果没有坐标数据则返回
if (!recognizeCoordinates.value || recognizeCoordinates.value.length === 0) {
console.warn('No recognition coordinates to draw')
return
}
// 绘制所有识别框
recognizeCoordinates.value.forEach(item => {
if (item.bbox && item.bbox.length === 4) {
drawBoundingBox(ctx, item.bbox, item.label, item.score)
}
})
} catch (error) {
console.error('Error drawing bounding boxes:', error)
}
})
}
// 修改绘制函数,添加参数验证
const drawBoundingBox = (ctx, bbox, label, score) => {
if (!bbox || bbox.length !== 4) return
const [x1, y1, x2, y2] = bbox
const width = x2 - x1
const height = y2 - y1
// 验证坐标是否有效
if (width <= 0 || height <= 0) return
// 绘制矩形框
ctx.strokeStyle = '#FF0000'
ctx.lineWidth = 2
ctx.strokeRect(x1, y1, width, height)
// 绘制标签背景
ctx.fillStyle = '#FF0000'
ctx.globalAlpha = 0.7
ctx.fillRect(x1, y1, Math.max(80, width * 0.3), 20)
ctx.globalAlpha = 1
// 绘制标签文本
ctx.fillStyle = '#FFFFFF'
ctx.font = '12px Arial'
const labelText = `${label || 'unknown'} ${(parseFloat(score) * 100).toFixed(0)}%`
ctx.fillText(labelText, x1 + 5, y1 + 15)
}
// 添加对图片的额外监听
watch(imgRef, (newImg) => {
if (newImg) {
if (newImg.complete) {
onImageLoad()
} else {
newImg.onload = onImageLoad
}
}
})
// 确保在坐标数据变化时重新绘制
watch(() => recognizeCoordinates.value, (newVal) => {
if (newVal && imgRef.value && imgRef.value.complete) {
onImageLoad()
}
}, { deep: true })
</script>
<style lang="less" scoped>
.EventAlarmDetail {
padding-bottom: 20px;
}
.EventAlarmDetail .topPart {
padding: 16px;
}
.header_right {
display: flex;
}
.header_right img {
width: 48px;
height: 48px;
margin-right: 16px;
}
.header_right h5 {
height: 14px;
font-size: 14px;
color: #333330;
font-weight: 400;
margin-bottom: 14px;
}
.header_right .num_header {
display: inline-block;
width: 31px;
height: 18px;
line-height: 18px;
text-align: center;
background: rgba(0, 86, 245, 0.1);
border-radius: 3px;
font-size: 10px;
color: #0056F5;
font-weight: 400;
margin-right: 8px;
}
.header_right p {
height: 10px;
font-size: 10px;
color: #0056F5;
font-weight: 400;
margin-bottom: 2px;
}
:deep(.ant-page-header .ant-page-header-content) {
padding: 0;
}
:deep(.ant-descriptions .ant-descriptions-title) {
margin-top: 15px!important;
}
.listManagePage .topPart {
display: flex;
align-items: center;
margin: 0 16px 16px 0;
background: rgba(255, 255, 255, 0.70);
border: 1px solid #FFFFFF;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.10);
border-radius: 3px;
}
.footer_btn {
position: fixed;
bottom: 0;
right: 0;
box-sizing: border-box;
margin-right: 16px;
background: #fff;
padding-bottom: -16px;
text-align: right;
width: 100%;
padding: 12px 17px 12px 0;
border: 1px solid #FFFFFF;
box-shadow: 0 -2px 6px 0 rgba(0,27,77,0.06);
}
.topForm {
padding: 6px 16px;
background: #F3F7FE;
}
.firstForm {
margin-bottom: 24px;
}
.secform {
padding: 16px 16px 36px;
}
.tableTitleLine {
margin-bottom: 14px;
}
:deep(.ant-form-item .ant-form-item-control-input-content) {
font-size: 12px;
color: #666666;
font-weight: 400;
}
:deep(.ant-input:placeholder-shown) {
font-size: 12px;
color: #DBDFE8;
font-weight: 400;
}
.img_size {
// width:100%;
height:200px;
}
:deep(.ant-image .ant-image-img) {
width: 350px;
height: 200px;
}
:deep(.ant-form-item .ant-form-item-explain-error) {
font-size: 12px;
}
.image-container {
position: relative;
display: inline-block;
line-height: 0; /* 防止图片下方有间隙 */
}
.overlay-canvas {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
z-index: 2; /* 确保canvas在图片上方 */
}
.img_size {
display: block;
max-width: 100%;
height:200px;
position: relative;
z-index: 1;
}
</style>
在事件图片的效果上再实现可以图片预览,预览图片的时候,绘制图也跟着变化