fix: 优化电子围栏绘制!

This commit is contained in:
liuzhiyuan 2025-08-01 16:21:53 +08:00
parent 7a0e0615c8
commit 6e7e065763
2 changed files with 643 additions and 463 deletions

View File

@ -13,7 +13,7 @@
</svg>
<p>视频区域</p>
<!-- 绘制蒙版层 -->
<div class="drawing-mask" v-if="eventData.fenceArea === '自定义画面'">
<div class="drawing-mask" v-if="activeEvent.fenceArea === '自定义画面'">
<div class="mask-background"></div>
<canvas
ref="drawingCanvas"
@ -66,7 +66,7 @@
<!-- 顶部标题和新增按钮 -->
<div class="section-header">
<h3>事件绑定与算法绑定</h3>
<el-button class="add-event-btn">
<el-button class="add-event-btn" @click="addEventGroup">
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</svg>
@ -74,19 +74,32 @@
</el-button>
</div>
<!-- 单个事件组 -->
<div class="event-group">
<!-- 事件组列表 -->
<div class="event-group-list">
<div
class="event-group"
v-for="(event, index) in eventGroups"
:key="index"
:class="{ 'active': activeIndex === index }"
@click="switchEvent(index)"
>
<!-- 第一行事件名称电子围栏区域告警等级 -->
<div class="form-row">
<div class="form-item">
<label>事件名称:</label>
<el-input v-model="eventData.name" placeholder="船舶登轮" />
<el-select v-model="event.name" placeholder="请选择事件类型">
<el-option label="船舶靠泊" value="船舶靠泊" />
<el-option label="船舶离泊" value="船舶离泊" />
<el-option label="人员登轮" value="人员登轮" />
<el-option label="人员离轮" value="人员离轮" />
<el-option label="电脑弹窗" value="电脑弹窗" />
</el-select>
</div>
<div class="form-item">
<label>电子围栏区域:</label>
<el-switch
v-model="eventData.fenceArea"
v-model="event.fenceArea"
active-text="整个监控画面"
inactive-text="自定义画面"
active-value="整个监控画面"
@ -96,7 +109,7 @@
<div class="form-item">
<label>告警等级:</label>
<el-select v-model="eventData.level" placeholder="P0紧急告警">
<el-select v-model="event.level" placeholder="P0紧急告警">
<el-option label="P0紧急告警" value="P0" />
<el-option label="P1重要告警" value="P1" />
<el-option label="P2一般告警" value="P2" />
@ -106,37 +119,42 @@
</div>
<!-- 动态生成的电子围栏配置区域 -->
<div class="form-row" v-for="(fence, index) in fenceAreas" :key="index">
<div class="form-item">
<label>电子围栏名称 {{ index + 1 }}:</label>
<el-input v-model="fence.name" placeholder="请输入名称" />
<div class="form-row" v-for="(fence, fenceIndex) in event.fenceAreas" :key="fenceIndex">
<div class="form-item" style="width: 20%;">
<label>电子围栏名称:</label>
<el-input v-model="fence.name" placeholder="请输入" />
</div>
<div class="form-item">
<label>绑定算法 {{ index + 1 }}:</label>
<el-select v-model="fence.algorithm" placeholder="选择算法">
<div class="form-item" style="width: 30%;">
<label>算法绑定:</label>
<el-select v-model="fence.algorithm" placeholder="算法XXX001">
<el-option v-for="alg in algorithms"
:key="alg.id"
:label="alg.name"
:value="alg.id" />
</el-select>
</div>
<div class="form-item" tyle="width: 50%;">
<label>算法条件:</label>
<label style="width:80%;">{{fence.condition}}</label>
</div>
</div>
<!-- 第三行交叉区域名称交集算法绑定 -->
<div class="form-row">
<div class="form-item">
<div class="form-item" style="width: 20%;">
<label>交叉区域名称:</label>
<el-input v-model="eventData.crossAreaName" placeholder="请输入" />
<el-input v-model="event.crossAreaName" placeholder="请输入" />
</div>
<div class="form-item">
<div class="form-item" tyle="width: 30%;">
<label>交集算法绑定:</label>
<el-select v-model="eventData.crossAlgorithm" placeholder="算法XXX001">
<el-select v-model="event.crossAlgorithm" placeholder="算法XXX001">
<el-option v-for="alg in algorithms" :key="alg.id" :label="alg.name" :value="alg.id" />
</el-select>
</div>
</div>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="editor-footer">
@ -203,7 +221,7 @@
</template>
<script setup>
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, watch, onMounted, onBeforeUnmount, nextTick, computed } from 'vue'
import { UploadFilled, Delete } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
@ -218,7 +236,6 @@ const canvasInitialized = ref(false)
const points = ref([]) //
const completedShapes = ref([]) //
const isDrawing = ref(false) //
const fenceAreas = ref([]) //
const selectedShapeIndex = ref(null) //
const drawMode = ref('polygon') // polygon-line-线
const mousePos = ref(null) //
@ -233,6 +250,10 @@ const uploadDialogVisible = ref(false)
const fileList = ref([])
const selectedFile = ref(null)
//
const eventGroups = ref([])
const activeIndex = ref(0)
const props = defineProps({
deviceData: {
type: Object,
@ -243,74 +264,165 @@ const props = defineProps({
const emit = defineEmits(['close', 'save'])
const algorithms = ref([
{ id: 'alg-001', name: '算法XXX001' },
{ id: 'alg-002', name: '算法XXX002' },
{ id: 'alg-003', name: '算法XXX003' }
{ id: 'alg-001', name: '船舶靠泊识别', condition: '识别船舶框体与电子围栏相交则报警靠岸成功' },
{ id: 'alg-002', name: '船舶离泊识别', condition: '识别船舶框体与电子围栏远离则报警开始离岸' },
{ id: 'alg-003', name: '人员登轮识别', condition: '目标识别人员依次通过电子围栏1、2则报警人员登轮' },
{ id: 'alg-004', name: '人员离轮识别', condition: '目标识别人员依次通过电子围栏1、2则报警人员离轮' },
{ id: 'alg-005', name: '弹窗识别', condition: '识别个人办公区域内电脑出现弹窗后无第二位员工且弹窗消失则报警' }
])
const eventData = ref({
name: props.deviceData.eventName || '船舶登轮',
//
const activeEvent = computed(() => {
return eventGroups.value[activeIndex.value] || {}
})
//
const initEventGroups = () => {
eventGroups.value = [
{
name: '船舶靠泊', //
fenceArea: '整个监控画面',
level: 'P0',
fenceName: '',
algorithm: 'alg-001',
fenceAreas: [],
completedShapes: [],
points: [],
drawMode: 'polygon',
crossAreaName: '',
crossAlgorithm: 'alg-001'
})
//
const eventData1 = ref({
name: '船舶登轮',
area: '整个监控画面',
level: 'P0',
fenceName: '',
algorithm: 'alg-001',
crossAreaName: '',
crossAlgorithm: 'alg-001'
})
//
const eventData2 = ref({
name: '船舶登轮',
area: '整个监控画面',
algorithm: 'alg-001'
})
//
const handleUploadClick = () => {
uploadDialogVisible.value = true
fileList.value = []
selectedFile.value = null
}
const handleVideoChange = (file) => {
selectedFile.value = file.raw
}
const confirmUpload = () => {
if (!selectedFile.value) return
//
console.log('上传文件:', selectedFile.value)
//
ElMessage.success('视频上传成功')
uploadDialogVisible.value = false
}
const saveEvent = () => {
const allEventData = {
event1: eventData1.value,
event2: eventData2.value,
fenceAreas: fenceAreas.value
}
emit('save', allEventData)
]
}
const closeEditor = () => {
emit('close')
//
const addEventGroup = () => {
eventGroups.value.push({
name: '船舶靠泊', //
fenceArea: '整个监控画面',
level: 'P0',
fenceAreas: [],
completedShapes: [],
points: [],
drawMode: 'polygon',
crossAreaName: '',
crossAlgorithm: 'alg-001'
})
//
switchEvent(eventGroups.value.length - 1)
}
//
const switchEvent = (index) => {
//
if (activeEvent.value) {
activeEvent.value.points = [...points.value];
activeEvent.value.completedShapes = [...completedShapes.value];
activeEvent.value.drawMode = drawMode.value;
}
//
activeIndex.value = index;
//
points.value = [...(activeEvent.value.points || [])];
completedShapes.value = [...(activeEvent.value.completedShapes || [])];
drawMode.value = activeEvent.value.drawMode || 'polygon';
//
showDrawingCanvas.value = activeEvent.value.fenceArea === '自定义画面';
//
nextTick(() => {
if (showDrawingCanvas.value) {
initCanvas();
}
redrawCanvas();
});
}
//
const saveEvent = () => {
//
if (activeEvent.value) {
activeEvent.value.points = [...points.value]
activeEvent.value.completedShapes = [...completedShapes.value]
}
emit('save', {
eventGroups: eventGroups.value
})
}
//
onMounted(() => {
initEventGroups()
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
//
watch(() => activeEvent.value?.name, (newName, oldName) => {
if (newName !== oldName) {
//
updateEventConfigByType(newName);
}
})
const updateEventConfigByType = (eventType) => {
if (!activeEvent.value) return;
//
const currentPoints = [...points.value];
const currentShapes = [...completedShapes.value];
//
let algorithmId = 'alg-001';
let fenceName = '围栏区域';
switch(eventType) {
case '船舶靠泊':
algorithmId = 'alg-001';
fenceName = '岸边';
break;
case '船舶离泊':
algorithmId = 'alg-002';
fenceName = '岸边';
break;
case '人员登轮':
algorithmId = 'alg-003';
fenceName = '岸边平面';
break;
case '人员离轮':
algorithmId = 'alg-004';
fenceName = '船舶楼梯上层平面';
break;
case '电脑弹窗':
algorithmId = 'alg-005';
fenceName = '个人办公区域';
break;
}
//
activeEvent.value.crossAlgorithm = algorithmId;
//
points.value = currentPoints;
completedShapes.value = currentShapes;
//
redrawCanvas();
}
//
const handleResize = () => {
if (showDrawingCanvas.value && canvasInitialized.value) {
initCanvas()
redrawCanvas()
}
}
//
const initCanvas = () => {
if (!drawingCanvas.value || !videoContainer.value) return
@ -322,34 +434,6 @@ const initCanvas = () => {
canvasInitialized.value = true
}
//
watch(() => eventData.value.fenceArea, (newVal) => {
showDrawingCanvas.value = newVal === '自定义画面'
if (showDrawingCanvas.value) {
nextTick(() => {
initCanvas()
resetDrawing()
})
}
})
//
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
//
const handleResize = () => {
if (showDrawingCanvas.value && canvasInitialized.value) {
initCanvas()
redrawCanvas()
}
}
//
const handleMouseMove = (e) => {
if (!isDrawing.value || points.value.length === 0) return
@ -364,7 +448,7 @@ const handleMouseMove = (e) => {
//
const handleCanvasClick = (e) => {
if (eventData.value.fenceArea !== '自定义画面') return
if (activeEvent.value?.fenceArea !== '自定义画面') return
const rect = drawingCanvas.value.getBoundingClientRect()
const x = e.clientX - rect.left
@ -420,41 +504,170 @@ const handleCanvasClick = (e) => {
redrawCanvas()
}
//
// completeDrawing
const completeDrawing = () => {
if (points.value.length < 2) {
isDrawing.value = false
points.value = []
return
isDrawing.value = false;
points.value = [];
return;
}
// 2
if (drawMode.value === 'polygon' && points.value.length > 2) {
const firstPoint = points.value[0]
const lastPoint = points.value[points.value.length - 1]
const firstPoint = points.value[0];
const lastPoint = points.value[points.value.length - 1];
//
if (firstPoint.x !== lastPoint.x || firstPoint.y !== lastPoint.y) {
points.value.push({x: firstPoint.x, y: firstPoint.y})
points.value.push({x: firstPoint.x, y: firstPoint.y});
}
}
completedShapes.value.push({
//
const newShape = {
type: drawMode.value,
points: [...points.value]
})
};
//
fenceAreas.value.push({
name: `${drawMode.value === 'polygon' ? '围栏区域' : '直线'} ${fenceAreas.value.length + 1}`,
algorithm: 'alg-001',
//
completedShapes.value.push(newShape);
//
let fenceName = '围栏区域';
let algorithmId = 'alg-001';
if (activeEvent.value.name === '船舶靠泊') {
fenceName = '岸边';
algorithmId = 'alg-001';
} else if (activeEvent.value.name === '船舶离泊') {
fenceName = '岸边';
algorithmId = 'alg-002';
} else if (activeEvent.value.name === '人员登轮') {
fenceName = '岸边平面';
algorithmId = 'alg-003';
} else if (activeEvent.value.name === '人员离轮') {
fenceName = '船舶楼梯上层平面';
algorithmId = 'alg-004';
} else if (activeEvent.value.name === '电脑弹窗') {
fenceName = '个人办公区域';
algorithmId = 'alg-005';
}
//
if (activeEvent.value) {
activeEvent.value.fenceAreas.push({
name: `${fenceName}`,
algorithm: algorithmId,
points: [...points.value],
type: drawMode.value
type: drawMode.value,
condition: algorithms.value.find(a => a.id === algorithmId)?.condition || ''
});
// completedShapesfenceAreas
activeEvent.value.completedShapes = [...completedShapes.value];
}
//
points.value = [];
isDrawing.value = false;
//
redrawCanvas();
}
// redrawCanvas
const redrawCanvas = () => {
if (!ctx.value || !drawingCanvas.value) return
//
ctx.value.clearRect(0, 0, drawingCanvas.value.width, drawingCanvas.value.height)
//
completedShapes.value.forEach((shape, index) => {
//
if (index === selectedShapeIndex.value) {
ctx.value.fillStyle = 'rgba(255, 0, 0, 0.3)'
ctx.value.strokeStyle = '#ff0000'
} else {
ctx.value.fillStyle = 'rgba(124, 208, 255, 0.3)'
ctx.value.strokeStyle = '#7cd0ff'
}
//
drawShape(shape)
})
//
if (points.value.length > 0) {
//
points.value.forEach((point, index) => {
ctx.value.beginPath()
ctx.value.arc(point.x, point.y, 5, 0, Math.PI * 2)
ctx.value.fillStyle = index === clickedPointIndex.value ? '#ff0000' : '#ff7cd0'
ctx.value.fill()
ctx.value.strokeStyle = '#fff'
ctx.value.lineWidth = 1
ctx.value.stroke()
})
// 线
if (points.value.length > 1 || mousePos.value) {
ctx.value.beginPath()
ctx.value.moveTo(points.value[0].x, points.value[0].y)
if (drawMode.value === 'polygon') {
for (let i = 1; i < points.value.length; i++) {
ctx.value.lineTo(points.value[i].x, points.value[i].y)
}
} else if (drawMode.value === 'line') {
const endPoint = mousePos.value || points.value[1] || points.value[0]
ctx.value.lineTo(endPoint.x, endPoint.y)
}
ctx.value.strokeStyle = '#7cd0ff'
ctx.value.lineWidth = 2
ctx.value.stroke()
}
}
}
// drawShape
const drawShape = (shape) => {
if (!shape || !shape.points || shape.points.length < 2) return
ctx.value.beginPath()
ctx.value.moveTo(shape.points[0].x, shape.points[0].y)
for (let i = 1; i < shape.points.length; i++) {
ctx.value.lineTo(shape.points[i].x, shape.points[i].y)
}
if (shape.type === 'polygon') {
ctx.value.closePath()
ctx.value.fill()
}
ctx.value.stroke()
//
shape.points.forEach(point => {
drawPoint(point)
})
}
// resetDrawing
const resetDrawing = () => {
points.value = []
completedShapes.value = []
if (activeEvent.value) {
activeEvent.value.fenceAreas = []
activeEvent.value.completedShapes = []
}
selectedShapeIndex.value = null
isDrawing.value = false
redrawCanvas()
if (ctx.value && drawingCanvas.value) {
ctx.value.clearRect(0, 0, drawingCanvas.value.width, drawingCanvas.value.height)
}
}
//
@ -514,7 +727,9 @@ const deleteSelectedShape = () => {
//
completedShapes.value.splice(selectedShapeIndex.value, 1)
//
fenceAreas.value.splice(selectedShapeIndex.value, 1)
if (activeEvent.value) {
activeEvent.value.fenceAreas.splice(selectedShapeIndex.value, 1)
}
selectedShapeIndex.value = null
redrawCanvas()
@ -525,7 +740,7 @@ const isPointInShape = (point, shape) => {
if (shape.type === 'polygon') {
return isPointInPolygon(point, shape.points)
} else if (shape.type === 'line') {
return isPointNearLine(point, shape.points[0, shape.points[1]])
return isPointNearLine(point, shape.points[0], shape.points[1])
}
return false
}
@ -587,88 +802,9 @@ const isPointNearLine = (point, lineStart, lineEnd) => {
return distance < 15 // 15线
}
//
const redrawCanvas = () => {
if (!ctx.value || !drawingCanvas.value) return
ctx.value.clearRect(0, 0, drawingCanvas.value.width, drawingCanvas.value.height)
//
completedShapes.value.forEach((shape, index) => {
//
if (index === selectedShapeIndex.value) {
ctx.value.fillStyle = 'rgba(255, 0, 0, 0.3)'
ctx.value.strokeStyle = '#ff0000'
} else {
ctx.value.fillStyle = 'rgba(124, 208, 255, 0.3)'
ctx.value.strokeStyle = '#7cd0ff'
}
drawShape(shape)
})
//
if (points.value.length > 0) {
//
points.value.forEach((point, index) => {
ctx.value.beginPath()
ctx.value.arc(point.x, point.y, 5, 0, Math.PI * 2)
//
ctx.value.fillStyle = index === clickedPointIndex.value ? '#ff0000' : '#ff7cd0'
ctx.value.fill()
ctx.value.strokeStyle = '#fff'
ctx.value.lineWidth = 1
ctx.value.stroke()
})
// 线线
if (points.value.length > 1 || mousePos.value) {
ctx.value.beginPath()
ctx.value.moveTo(points.value[0].x, points.value[0].y)
// 线
if (drawMode.value === 'polygon') {
for (let i = 1; i < points.value.length; i++) {
ctx.value.lineTo(points.value[i].x, points.value[i].y)
}
}
// 线线
if (drawMode.value === 'line') {
const endPoint = mousePos.value || points.value[1] || points.value[0]
ctx.value.lineTo(endPoint.x, endPoint.y)
}
ctx.value.strokeStyle = '#7cd0ff'
ctx.value.lineWidth = 2
ctx.value.stroke()
}
}
}
//
const drawShape = (shape) => {
if (shape.points.length < 2) return
ctx.value.beginPath()
ctx.value.moveTo(shape.points[0].x, shape.points[0].y)
for (let i = 1; i < shape.points.length; i++) {
ctx.value.lineTo(shape.points[i].x, shape.points[i].y)
}
if (shape.type === 'polygon') {
ctx.value.closePath()
ctx.value.fill()
}
ctx.value.stroke()
//
shape.points.forEach(point => {
drawPoint(point)
})
}
//
const drawPoint = (point) => {
@ -681,16 +817,32 @@ const drawPoint = (point) => {
ctx.value.stroke()
}
//
const resetDrawing = () => {
points.value = []
completedShapes.value = []
fenceAreas.value = []
selectedShapeIndex.value = null
isDrawing.value = false
if (ctx.value && drawingCanvas.value) {
ctx.value.clearRect(0, 0, drawingCanvas.value.width, drawingCanvas.value.height)
}
//
const handleUploadClick = () => {
uploadDialogVisible.value = true
fileList.value = []
selectedFile.value = null
}
const handleVideoChange = (file) => {
selectedFile.value = file.raw
}
const confirmUpload = () => {
if (!selectedFile.value) return
//
console.log('上传文件:', selectedFile.value)
//
ElMessage.success('视频上传成功')
uploadDialogVisible.value = false
}
const closeEditor = () => {
emit('close')
}
</script>
@ -700,7 +852,6 @@ const resetDrawing = () => {
background: #0a1d3c;
padding: 20px;
width: 100vw;
}
.editor-content {
@ -851,11 +1002,34 @@ const resetDrawing = () => {
gap: 6px;
}
.event-group-list {
flex: 1;
overflow-y: auto;
padding-right: 10px;
}
.event-group {
background: rgba(5, 18, 42, 0.6);
border-radius: 4px;
padding: 15px;
margin-bottom: 15px;
transition: all 0.3s;
}
.event-group.active {
border: 1px solid #00d2ff;
}
.algorithm-condition-hint {
margin-top: 15px;
padding: 8px 12px;
background-color: rgba(255, 77, 79, 0.2);
border-radius: 4px;
}
.hint-text {
color: #ff4d4f;
font-size: 14px;
}
.form-row {
@ -944,6 +1118,7 @@ const resetDrawing = () => {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 15px;
}
.delete-polygon-btn {

View File

@ -4,6 +4,11 @@ import path from 'path'
export default defineConfig({
plugins: [vue()],
server: {
host: '0.0.0.0', // 监听所有网络接口
port: 5173, // 默认端口
strictPort: true // 如果端口被占用则退出
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),