|
@@ -216,8 +216,56 @@
|
|
|
</a-form-item-rest>
|
|
|
</a-form-item>
|
|
|
|
|
|
- <a-form-item label="检测区域" name="region">
|
|
|
- 检测区域选择组件 {{ formState.region }}
|
|
|
+ <a-form-item
|
|
|
+ v-if="[1, 2, 3, 9].includes(formState?.eventType as number)"
|
|
|
+ label="检测区域"
|
|
|
+ name="region"
|
|
|
+ >
|
|
|
+ 框选区域 {{ formState.region }}
|
|
|
+ <a-form-item-rest>
|
|
|
+ <div class="viewer">
|
|
|
+ <div class="viewer-content">
|
|
|
+ <div
|
|
|
+ class="mapBox blockArea"
|
|
|
+ :style="{
|
|
|
+ width: `${areaWidth}px`,
|
|
|
+ height: `${areaHeight}px`,
|
|
|
+ cursor: 'default',
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <!-- 已创建区块 -->
|
|
|
+ <div
|
|
|
+ v-for="(block, blockIndex) in blocks"
|
|
|
+ :key="blockIndex"
|
|
|
+ class="block-item"
|
|
|
+ :style="{
|
|
|
+ left: `${block.x}px`,
|
|
|
+ top: `${block.y}px`,
|
|
|
+ width: `${block.width}px`,
|
|
|
+ height: `${block.height}px`,
|
|
|
+ border: `2px solid #1890ff`,
|
|
|
+ position: 'absolute',
|
|
|
+ cursor: 'move',
|
|
|
+ backgroundColor: 'rgba(24, 144, 255, 0.1)',
|
|
|
+ }"
|
|
|
+ @mousedown="startDrag(block, $event)"
|
|
|
+ @mousemove="drag(block)"
|
|
|
+ @mouseup="endDrag(block)"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="resize-handle"
|
|
|
+ :style="{
|
|
|
+ backgroundColor: '#1890ff',
|
|
|
+ }"
|
|
|
+ @mousedown.stop="startResize(block, $event)"
|
|
|
+ >
|
|
|
+ <!-- {{ blockIndex + 1 }} -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </a-form-item-rest>
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="是否启用">
|
|
@@ -239,6 +287,7 @@
|
|
|
import { ref, reactive, watch, computed } from 'vue'
|
|
|
import { message, type FormInstance } from 'ant-design-vue'
|
|
|
import * as alarmApi from '@/api/alarm'
|
|
|
+import { furnitureIconSizeMap } from '@/const/furniture'
|
|
|
|
|
|
defineOptions({
|
|
|
name: 'AlarmPlanModal',
|
|
@@ -283,6 +332,11 @@ type Props = {
|
|
|
clientId: string // 设备ID
|
|
|
alarmPlanId?: number | null // 告警计划ID 编辑时传入
|
|
|
data?: AlarmPlan // 编辑数据
|
|
|
+ area?: {
|
|
|
+ width: number
|
|
|
+ height: number
|
|
|
+ ranges: number[]
|
|
|
+ }
|
|
|
}
|
|
|
const emit = defineEmits<{
|
|
|
(e: 'update:open', value: boolean): void
|
|
@@ -301,6 +355,145 @@ const modelTitle = computed(() => {
|
|
|
return props.alarmPlanId ? '编辑告警计划' : '新增告警计划'
|
|
|
})
|
|
|
|
|
|
+// 检测区域宽度
|
|
|
+const areaWidth = computed(() => {
|
|
|
+ return Math.abs(props.area?.width || 0)
|
|
|
+})
|
|
|
+// 检测区域高度
|
|
|
+const areaHeight = computed(() => {
|
|
|
+ return Math.abs(props.area?.height || 0)
|
|
|
+})
|
|
|
+
|
|
|
+interface BlockItem {
|
|
|
+ // 本地用
|
|
|
+ x: number // 区块基于父元素的X偏移量,区块的左上角x坐标
|
|
|
+ y: number // 区块基于父元素的Y偏移量,区块的左上角y坐标
|
|
|
+ ox: number // 区块基于原点的X偏移量,区块的左上角x坐标
|
|
|
+ oy: number // 区块基于原点的Y偏移量,区块的左上角y坐标
|
|
|
+ width: number // 区块宽度
|
|
|
+ height: number // 区块高度
|
|
|
+}
|
|
|
+
|
|
|
+const blocks = ref<BlockItem[]>([])
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取坐标位置
|
|
|
+ * @param offsetLeft 元素基于父容器的X坐标
|
|
|
+ * @param offsetTop 元素基于父容器的Y坐标
|
|
|
+ */
|
|
|
+const getOriginPosition = ([offsetLeft, offsetTop]: number[] = [0, 0]) => {
|
|
|
+ const [xstart, xend, ystart, yend] = props.area!.ranges
|
|
|
+
|
|
|
+ // 容器宽高
|
|
|
+ const containerWidth = Math.abs(xstart) + Math.abs(xend)
|
|
|
+ const containerHeight = Math.abs(ystart) + Math.abs(yend)
|
|
|
+
|
|
|
+ // 原点在容器中的坐标
|
|
|
+ const originX = Math.abs(xstart)
|
|
|
+ const originY = Math.abs(yend)
|
|
|
+
|
|
|
+ // 元素基于父容器的偏移量
|
|
|
+ const offsetX = offsetLeft ?? 0
|
|
|
+ const offsetY = offsetTop ?? 0
|
|
|
+
|
|
|
+ // 元素基于原点的偏移量
|
|
|
+ const originOffsetX = offsetX - originX
|
|
|
+ const originOffsetY = originY - offsetY
|
|
|
+
|
|
|
+ // 雷达尺寸
|
|
|
+ const radarWidth = furnitureIconSizeMap['radar']?.width ?? 0
|
|
|
+ const radarHeight = furnitureIconSizeMap['radar']?.height ?? 0
|
|
|
+
|
|
|
+ // 雷达基于原点的偏移量
|
|
|
+ const radarX = Math.round(originX - radarWidth / 2)
|
|
|
+ const radarY = Math.round(originY - radarHeight / 2)
|
|
|
+
|
|
|
+ const data = {
|
|
|
+ width: containerWidth, // 容器宽度
|
|
|
+ height: containerHeight, // 容器高度
|
|
|
+ originX: Math.round(originX), // 原点X坐标
|
|
|
+ originY: Math.round(originY), // 原点Y坐标
|
|
|
+ offsetX: Math.round(offsetX), // 元素基于父容器的偏移量 X坐标
|
|
|
+ offsetY: Math.round(offsetY), // 元素基于父容器的偏移量 Y坐标
|
|
|
+ originOffsetX: Math.round(originOffsetX), // 元素基于原点的偏移量 X坐标
|
|
|
+ originOffsetY: Math.round(originOffsetY), // 元素基于原点的偏移量 Y坐标
|
|
|
+ radarX, // 雷达X坐标
|
|
|
+ radarY, // 雷达Y坐标
|
|
|
+ radarWidth, // 雷达宽度
|
|
|
+ radarHeight, // 雷达高度
|
|
|
+ }
|
|
|
+
|
|
|
+ return data
|
|
|
+}
|
|
|
+
|
|
|
+// 区块拖动
|
|
|
+const startDrag = (block: BlockItem, e: MouseEvent) => {
|
|
|
+ console.log('startDrag', block)
|
|
|
+ e.stopPropagation()
|
|
|
+ const container = document.querySelector('.blockArea') as HTMLElement
|
|
|
+ const rect = container.getBoundingClientRect()
|
|
|
+ const offsetX = e.clientX - rect.left - block.x
|
|
|
+ const offsetY = e.clientY - rect.top - block.y
|
|
|
+
|
|
|
+ const moveHandler = (e: MouseEvent) => {
|
|
|
+ const newX = e.clientX - rect.left - offsetX
|
|
|
+ const newY = e.clientY - rect.top - offsetY
|
|
|
+ const containerWidth = container.offsetWidth
|
|
|
+ const containerHeight = container.offsetHeight
|
|
|
+
|
|
|
+ block.x = Math.max(0, Math.min(newX, containerWidth - block.width))
|
|
|
+ block.y = Math.max(0, Math.min(newY, containerHeight - block.height))
|
|
|
+ block.ox = block.x - getOriginPosition().originX
|
|
|
+ block.oy = getOriginPosition().originY - block.y
|
|
|
+ }
|
|
|
+
|
|
|
+ const upHandler = () => {
|
|
|
+ document.removeEventListener('mousemove', moveHandler)
|
|
|
+ document.removeEventListener('mouseup', upHandler)
|
|
|
+ }
|
|
|
+
|
|
|
+ document.addEventListener('mousemove', moveHandler)
|
|
|
+ document.addEventListener('mouseup', upHandler)
|
|
|
+}
|
|
|
+
|
|
|
+const drag = (block: BlockItem) => {
|
|
|
+ formState.region = [block.ox, block.oy, block.width, block.height]
|
|
|
+}
|
|
|
+
|
|
|
+const endDrag = (block: BlockItem) => {
|
|
|
+ formState.region = [block.ox, block.oy, block.width, block.height]
|
|
|
+}
|
|
|
+
|
|
|
+// 获取容器边界
|
|
|
+const getContainerRect = () => {
|
|
|
+ const container = document.querySelector('.blockArea') as HTMLElement
|
|
|
+ return container?.getBoundingClientRect() || { left: 0, top: 0 }
|
|
|
+}
|
|
|
+
|
|
|
+const startResize = (block: BlockItem, e: MouseEvent) => {
|
|
|
+ const startX = e.clientX
|
|
|
+ const startY = e.clientY
|
|
|
+ const initialWidth = block.width
|
|
|
+ const initialHeight = block.height
|
|
|
+
|
|
|
+ const moveHandler = (e: MouseEvent) => {
|
|
|
+ const rect = getContainerRect()
|
|
|
+ const deltaX = e.clientX - startX
|
|
|
+ const deltaY = e.clientY - startY
|
|
|
+ // 限制最小尺寸和容器边界
|
|
|
+ block.width = Math.max(50, Math.min(initialWidth + deltaX, rect.width - block.x))
|
|
|
+ block.height = Math.max(50, Math.min(initialHeight + deltaY, rect.height - block.y))
|
|
|
+ }
|
|
|
+
|
|
|
+ const upHandler = () => {
|
|
|
+ document.removeEventListener('mousemove', moveHandler)
|
|
|
+ document.removeEventListener('mouseup', upHandler)
|
|
|
+ }
|
|
|
+
|
|
|
+ document.addEventListener('mousemove', moveHandler)
|
|
|
+ document.addEventListener('mouseup', upHandler)
|
|
|
+}
|
|
|
+
|
|
|
const weekOptions = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
|
|
const monthOptions = [
|
|
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
|
|
@@ -327,7 +520,7 @@ type FormState = {
|
|
|
|
|
|
const formState = reactive<FormState>({
|
|
|
planName: '',
|
|
|
- region: [-200, 200, 400, 400],
|
|
|
+ region: [0, 0, 50, 50],
|
|
|
eventType: null,
|
|
|
thresholdTime: 300,
|
|
|
mergeTime: 30,
|
|
@@ -342,6 +535,20 @@ const formState = reactive<FormState>({
|
|
|
timeThreshold: 300,
|
|
|
})
|
|
|
|
|
|
+const initBlocks = () => {
|
|
|
+ blocks.value = [
|
|
|
+ {
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ ox: formState.region[0],
|
|
|
+ oy: formState.region[1],
|
|
|
+ width: formState.region[2],
|
|
|
+ height: formState.region[3],
|
|
|
+ },
|
|
|
+ ]
|
|
|
+}
|
|
|
+initBlocks()
|
|
|
+
|
|
|
const thresholdTimeFormat = ref<'s' | 'min' | 'hour' | 'day'>('s') // 触发阈值 额外选择器
|
|
|
const timeThresholdFormat = ref<'s' | 'min' | 'hour' | 'day'>('s') // 异常消失时间阈值 额外选择器
|
|
|
|
|
@@ -382,7 +589,7 @@ const numToWeekMap: Record<number, string> = {
|
|
|
watch(
|
|
|
() => formState.effectTimeRanges,
|
|
|
(val) => {
|
|
|
- checkState.indeterminate = !!val.length && val.length < plainOptions.value.length //设置全选按钮的半选状态
|
|
|
+ checkState.indeterminate = !!val.length && val.length < plainOptions.value.length // 设置全选按钮的半选状态
|
|
|
checkState.checkAll = val.length === plainOptions.value.length // 设置全选按钮的选中状态
|
|
|
},
|
|
|
{
|
|
@@ -481,6 +688,8 @@ watch(
|
|
|
formState.effectType = echoFormState(val).effectType
|
|
|
formState.effectTimeRanges = echoFormState(val).effectTimeRanges
|
|
|
formState.effectTimeFrames = echoFormState(val).effectTimeFrames
|
|
|
+ formState.region = echoFormState(val).region
|
|
|
+ initBlocks()
|
|
|
}
|
|
|
},
|
|
|
{ immediate: true }
|
|
@@ -631,7 +840,9 @@ const submit = () => {
|
|
|
mergeTime: Number(formState.mergeTime) || 30, // 归并时间
|
|
|
eventVal: formState.eventType as number, // 事件类型 与 param 有联动关系
|
|
|
param: JSON.stringify(paramData), // 事件参数 与 eventVal 有联动关系
|
|
|
- region: JSON.stringify(formState.region), // 检测区域
|
|
|
+ region: [1, 2, 3, 9].includes(formState.eventType as number)
|
|
|
+ ? JSON.stringify(formState.region)
|
|
|
+ : [], // 检测区域
|
|
|
enable: Number(formState.enable) as 0 | 1, // 是否启用 0否 1是
|
|
|
|
|
|
// 生效方式
|
|
@@ -664,7 +875,7 @@ const submit = () => {
|
|
|
message.warn('请选择生效方式的范围')
|
|
|
return
|
|
|
}
|
|
|
- // if (formState.region.length !== 4) {
|
|
|
+ // if ([1, 2, 3, 9].includes(formState.eventType as number) && formState.region.length !== 4) {
|
|
|
// message.warn('请选择检测区域')
|
|
|
// return
|
|
|
// }
|
|
@@ -738,4 +949,73 @@ const submit = () => {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+.viewer {
|
|
|
+ padding: 10px;
|
|
|
+ min-width: 500px;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ &-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding-bottom: 20px;
|
|
|
+
|
|
|
+ &-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 24px;
|
|
|
+ }
|
|
|
+ &-subtitle {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &-content {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.mapBox {
|
|
|
+ background-color: #e0e0e0;
|
|
|
+ background-image:
|
|
|
+ linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
|
|
|
+ linear-gradient(to right, rgba(0, 0, 0, 0.1) 1px, transparent 1px);
|
|
|
+ background-size: 20px 20px;
|
|
|
+ position: relative;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ // 添加黑边框
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: -5px;
|
|
|
+ left: -5px;
|
|
|
+ width: calc(100% + 10px);
|
|
|
+ height: calc(100% + 10px);
|
|
|
+ border: 5px solid rgba(0, 0, 0, 0.8);
|
|
|
+ box-sizing: border-box;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.block-item {
|
|
|
+ background: rgba(24, 144, 255, 0.1);
|
|
|
+
|
|
|
+ .resize-handle {
|
|
|
+ position: absolute;
|
|
|
+ right: -4px;
|
|
|
+ bottom: -4px;
|
|
|
+ width: 15px;
|
|
|
+ height: 15px;
|
|
|
+ background: #1890ff;
|
|
|
+ cursor: nwse-resize;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #fff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|