|
|
@@ -3,7 +3,7 @@
|
|
|
<a-modal
|
|
|
:get-container="() => $refs.mod"
|
|
|
:open="props.open"
|
|
|
- :title="title"
|
|
|
+ :title="modelTitle"
|
|
|
:mask-closable="false"
|
|
|
width="600px"
|
|
|
@cancel="cancel"
|
|
|
@@ -29,17 +29,128 @@
|
|
|
/>
|
|
|
</a-form-item>
|
|
|
|
|
|
- <a-form-item label="检测区域" name="region"> 检测区域选择组件 </a-form-item>
|
|
|
+ <a-form-item
|
|
|
+ label="计划备注"
|
|
|
+ name="planName"
|
|
|
+ :rules="[{ required: true, message: '请输入计划备注' }]"
|
|
|
+ >
|
|
|
+ <a-input
|
|
|
+ v-model:value.trim="formState.remark"
|
|
|
+ placeholder="请输入备注"
|
|
|
+ :maxlength="200"
|
|
|
+ show-count
|
|
|
+ allow-clear
|
|
|
+ />
|
|
|
+ </a-form-item>
|
|
|
+
|
|
|
+ <a-form-item
|
|
|
+ label="触发阈值"
|
|
|
+ name="thresholdTime"
|
|
|
+ :rules="[{ required: true, message: '请输入触发阈值' }]"
|
|
|
+ >
|
|
|
+ <a-input-number
|
|
|
+ v-model:value.trim="formState.thresholdTime"
|
|
|
+ placeholder="请输入(默认300,需要大于0)"
|
|
|
+ min="0"
|
|
|
+ show-count
|
|
|
+ allow-clear
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <template #addonAfter>
|
|
|
+ <a-select v-model:value="thresholdTimeFormat" style="width: 80px">
|
|
|
+ <a-select-option value="s">秒</a-select-option>
|
|
|
+ <a-select-option value="min">分钟</a-select-option>
|
|
|
+ <a-select-option value="hour">小时</a-select-option>
|
|
|
+ <a-select-option value="day">天</a-select-option>
|
|
|
+ </a-select>
|
|
|
+ </template>
|
|
|
+ </a-input-number>
|
|
|
+ </a-form-item>
|
|
|
+
|
|
|
+ <a-form-item
|
|
|
+ label="归并时间"
|
|
|
+ name="mergeTime"
|
|
|
+ :rules="[{ required: true, message: '请输入归并时间' }]"
|
|
|
+ >
|
|
|
+ <a-input-number
|
|
|
+ v-model:value.trim="formState.mergeTime"
|
|
|
+ placeholder="请输入(默认30,需要大于0)"
|
|
|
+ min="0"
|
|
|
+ show-count
|
|
|
+ allow-clear
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ </a-form-item>
|
|
|
|
|
|
<a-form-item
|
|
|
label="事件类型"
|
|
|
name="eventType"
|
|
|
:rules="[{ required: true, message: '请选择事件类型' }]"
|
|
|
>
|
|
|
- <a-select v-model:value="formState.eventType" placeholder="请选择事件类型">
|
|
|
- <a-select-option value="1">跌倒</a-select-option>
|
|
|
- <a-select-option value="2">滞留</a-select-option>
|
|
|
- </a-select>
|
|
|
+ <a-select
|
|
|
+ v-model:value="formState.eventType"
|
|
|
+ :options="eventTypeList"
|
|
|
+ placeholder="请选择事件类型"
|
|
|
+ />
|
|
|
+
|
|
|
+ <a-form-item-rest v-if="formState.eventType && ![1, 2, 3].includes(formState.eventType)">
|
|
|
+ <div class="eventTypeBox">
|
|
|
+ <div v-if="[4, 5, 6, 7, 8].includes(formState.eventType)" class="eventTypeBox-item">
|
|
|
+ <span class="eventTypeBox-item-label">统计时间:</span>
|
|
|
+ <a-form-item
|
|
|
+ name="statisticsTime"
|
|
|
+ :rules="[{ required: true, message: '请选择统计时间' }]"
|
|
|
+ >
|
|
|
+ <a-time-range-picker
|
|
|
+ v-model:value="formState.statisticsTime"
|
|
|
+ valueFormat="HH:mm"
|
|
|
+ format="HH:mm"
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ </a-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="[6, 7].includes(formState.eventType)" class="eventTypeBox-item">
|
|
|
+ <span class="eventTypeBox-item-label">异常阈值:</span>
|
|
|
+ <a-form-item name="count" :rules="[{ required: true, message: '请输入异常阈值' }]">
|
|
|
+ <a-input-number
|
|
|
+ v-model:value.trim="formState.count"
|
|
|
+ placeholder="请输入(默认3,需要大于0)"
|
|
|
+ min="0"
|
|
|
+ show-count
|
|
|
+ allow-clear
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ </a-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="formState.eventType === 9" class="eventTypeBox-item">
|
|
|
+ <span class="eventTypeBox-item-labelend">异常消失时间阈值:</span>
|
|
|
+ <a-form-item
|
|
|
+ name="timeThreshold"
|
|
|
+ :rules="[{ required: true, message: '请输入异常消失时间阈值' }]"
|
|
|
+ >
|
|
|
+ <a-input-number
|
|
|
+ v-model:value.trim="formState.timeThreshold"
|
|
|
+ placeholder="请输入(默认300,需要大于0)"
|
|
|
+ min="0"
|
|
|
+ show-count
|
|
|
+ allow-clear
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <template #addonAfter>
|
|
|
+ <a-select v-model:value="timeThresholdFormat" style="width: 80px">
|
|
|
+ <a-select-option value="s">秒</a-select-option>
|
|
|
+ <a-select-option value="min">分钟</a-select-option>
|
|
|
+ <a-select-option value="hour">小时</a-select-option>
|
|
|
+ <a-select-option value="day">天</a-select-option>
|
|
|
+ </a-select>
|
|
|
+ </template>
|
|
|
+ </a-input-number>
|
|
|
+ </a-form-item>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </a-form-item-rest>
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item
|
|
|
@@ -50,45 +161,26 @@
|
|
|
<a-range-picker
|
|
|
v-model:value="formState.planTime"
|
|
|
style="width: 100%"
|
|
|
- show-time
|
|
|
- valueFormat="YYYY-MM-DD HH:mm:ss"
|
|
|
+ :show-time="false"
|
|
|
+ valueFormat="YYYY-MM-DD"
|
|
|
/>
|
|
|
</a-form-item>
|
|
|
|
|
|
- <a-form-item label="生效方式" name="firmwareVersion">
|
|
|
- <a-radio-group
|
|
|
- v-model:value="formState.effectType"
|
|
|
- name="radioGroup"
|
|
|
- @change="effectTypeChange"
|
|
|
- >
|
|
|
- <a-radio value="week">按周</a-radio>
|
|
|
- <a-radio value="mouth">按月</a-radio>
|
|
|
- </a-radio-group>
|
|
|
- </a-form-item>
|
|
|
-
|
|
|
- <a-form-item label="生效范围" name="firmwareVersion">
|
|
|
- <a-checkbox
|
|
|
- v-model:checked="checkState.checkAll"
|
|
|
- :indeterminate="checkState.indeterminate"
|
|
|
- @change="onCheckAllChange"
|
|
|
- >
|
|
|
- 全选
|
|
|
- </a-checkbox>
|
|
|
- <a-checkbox-group v-model:value="formState.effectTimeRanges" :options="plainOptions" />
|
|
|
- </a-form-item>
|
|
|
-
|
|
|
<a-form-item label="生效时段">
|
|
|
<div style="display: flex; align-items: center; gap: 8px">
|
|
|
<a-time-range-picker
|
|
|
v-model:value="formState.effectTimeFrame"
|
|
|
- valueFormat="HH:mm:ss"
|
|
|
+ valueFormat="HH:mm"
|
|
|
+ format="HH:mm"
|
|
|
style="width: 100%"
|
|
|
/>
|
|
|
<a-button size="small" type="link" @click="addEffectTime">添加</a-button>
|
|
|
</div>
|
|
|
<div style="margin-top: 12px">
|
|
|
- <span v-if="!formState.effectTimeFrames.length" style="color: #aaa; font-size: 12px"
|
|
|
- >暂无生效时段</span
|
|
|
+ <span
|
|
|
+ v-if="formState.effectTimeFrames && !formState.effectTimeFrames.length"
|
|
|
+ style="color: #aaa; font-size: 14px"
|
|
|
+ >⚠️暂无生效时段</span
|
|
|
>
|
|
|
<a-space wrap v-else>
|
|
|
<a-tag
|
|
|
@@ -102,8 +194,82 @@
|
|
|
</a-space>
|
|
|
</div>
|
|
|
</a-form-item>
|
|
|
+
|
|
|
+ <a-form-item label="生效方式">
|
|
|
+ <a-form-item-rest>
|
|
|
+ <a-radio-group
|
|
|
+ v-model:value="formState.effectType"
|
|
|
+ name="radioGroup"
|
|
|
+ @change="effectTypeChange"
|
|
|
+ >
|
|
|
+ <a-radio value="week">按周</a-radio>
|
|
|
+ <a-radio value="month">按月</a-radio>
|
|
|
+ </a-radio-group>
|
|
|
+ <a-checkbox
|
|
|
+ v-model:checked="checkState.checkAll"
|
|
|
+ :indeterminate="checkState.indeterminate"
|
|
|
+ @change="onCheckAllChange"
|
|
|
+ >
|
|
|
+ 全选
|
|
|
+ </a-checkbox>
|
|
|
+ <a-checkbox-group v-model:value="formState.effectTimeRanges" :options="plainOptions" />
|
|
|
+ </a-form-item-rest>
|
|
|
+ </a-form-item>
|
|
|
+
|
|
|
+ <a-form-item
|
|
|
+ v-if="[1, 2, 3, 9].includes(formState?.eventType as number)"
|
|
|
+ label="检测区域"
|
|
|
+ name="region"
|
|
|
+ style="user-select: none"
|
|
|
+ >
|
|
|
+ <span style="user-select: none">框选区域 {{ formState.region }}</span>
|
|
|
+ <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)"
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </a-form-item-rest>
|
|
|
+ </a-form-item>
|
|
|
+
|
|
|
<a-form-item label="是否启用">
|
|
|
- <a-switch v-model:checked="formState.isEnable" />
|
|
|
+ <a-switch v-model:checked="formState.enable" />
|
|
|
</a-form-item>
|
|
|
</a-form>
|
|
|
|
|
|
@@ -118,10 +284,10 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive, watch } from 'vue'
|
|
|
+import { ref, reactive, watch, computed } from 'vue'
|
|
|
import { message, type FormInstance } from 'ant-design-vue'
|
|
|
-// import * as deviceApi from '@/api/device'
|
|
|
-// import type { Rule } from 'ant-design-vue/es/form'
|
|
|
+import * as alarmApi from '@/api/alarm'
|
|
|
+import { getOriginPosition } from '@/utils/index'
|
|
|
|
|
|
defineOptions({
|
|
|
name: 'AlarmPlanModal',
|
|
|
@@ -129,9 +295,48 @@ defineOptions({
|
|
|
|
|
|
const formRef = ref<FormInstance>()
|
|
|
|
|
|
+type AlarmPlan = {
|
|
|
+ id: number
|
|
|
+ uuid: ID
|
|
|
+ name: string
|
|
|
+ clientId: string
|
|
|
+ enable: SwitchType
|
|
|
+ region: string
|
|
|
+ eventVal: number
|
|
|
+ alarmTimePlanId: ID
|
|
|
+ thresholdTime: ID
|
|
|
+ mergeTime: ID
|
|
|
+ param: string
|
|
|
+ createTime: string
|
|
|
+ updateTime: string
|
|
|
+ remark: string | null
|
|
|
+ alarmTimePlan: {
|
|
|
+ createId: ID
|
|
|
+ updateId: ID
|
|
|
+ createTime: ID
|
|
|
+ updateTime: ID
|
|
|
+ isDeleted: SwitchType | null
|
|
|
+ remark: ID
|
|
|
+ id: ID
|
|
|
+ startDate: string
|
|
|
+ stopDate: string
|
|
|
+ timeRange: string
|
|
|
+ monthDays: string
|
|
|
+ weekdays: string
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
type Props = {
|
|
|
open: boolean
|
|
|
title?: string
|
|
|
+ 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
|
|
|
@@ -141,68 +346,166 @@ const emit = defineEmits<{
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
open: false,
|
|
|
title: '告警计划',
|
|
|
+ clientId: '',
|
|
|
+ alarmPlanId: null,
|
|
|
+ data: undefined,
|
|
|
})
|
|
|
|
|
|
+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)
|
|
|
+})
|
|
|
+
|
|
|
+// 获取原点坐标
|
|
|
+const { originX, originY } = getOriginPosition(props.area!.ranges, [0, 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[]>([])
|
|
|
+
|
|
|
+// 区块拖动
|
|
|
+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 - originX
|
|
|
+ block.oy = 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(0, Math.min(initialWidth + deltaX, rect.width - block.x))
|
|
|
+ block.height = Math.max(0, 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 mouthOptions = [
|
|
|
- '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',
|
|
|
- '28',
|
|
|
- '29',
|
|
|
- '30',
|
|
|
- '31',
|
|
|
+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,
|
|
|
+ 28, 29, 30, 31,
|
|
|
]
|
|
|
|
|
|
type FormState = {
|
|
|
planName: string // 计划名称
|
|
|
- region: string[] // 检测区域 [left, top, width, height]
|
|
|
- eventType: string | null // 事件类型
|
|
|
+ region: number[] // 检测区域 [left, top, width, height]
|
|
|
+ eventType: number | null // 事件类型
|
|
|
+ thresholdTime: number | null // 触发阈值
|
|
|
+ mergeTime: number | null // 归并时间
|
|
|
planTime: string[] // 计划时间
|
|
|
effectType: 'week' | 'month' // 生效方式 周week、月month
|
|
|
- effectTimeRanges: string[] // 生效范围 周 1-7、月 1-31
|
|
|
+ effectTimeRanges: (number | string)[] // 生效范围 周 1-7、月 1-31
|
|
|
effectTimeFrame: string[] // 生效时段 单条 00:00:00 - 23:59:59
|
|
|
effectTimeFrames: { startTime: string; endTime: string }[] // 生效时段 多条
|
|
|
- isEnable: boolean // 是否启用
|
|
|
+ enable: boolean // 是否启用
|
|
|
+ statisticsTime: string[] // 统计时间
|
|
|
+ count?: number | null // 异常阈值
|
|
|
+ timeThreshold?: number | null // 异常消失时间阈值(单位:秒)
|
|
|
+ remark?: string // 备注
|
|
|
}
|
|
|
|
|
|
const formState = reactive<FormState>({
|
|
|
planName: '',
|
|
|
- region: [],
|
|
|
- eventType: '',
|
|
|
+ region: [0, 0, 50, 50],
|
|
|
+ eventType: null,
|
|
|
+ thresholdTime: 300,
|
|
|
+ mergeTime: 30,
|
|
|
planTime: [],
|
|
|
effectType: 'week',
|
|
|
effectTimeRanges: weekOptions,
|
|
|
effectTimeFrame: [],
|
|
|
effectTimeFrames: [],
|
|
|
- isEnable: true,
|
|
|
+ enable: true,
|
|
|
+ statisticsTime: [],
|
|
|
+ count: 3,
|
|
|
+ timeThreshold: 300,
|
|
|
})
|
|
|
|
|
|
-const plainOptions = ref<string[]>(weekOptions)
|
|
|
+const initBlocks = () => {
|
|
|
+ blocks.value = [
|
|
|
+ {
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ ox: formState.region[0] ?? 0,
|
|
|
+ oy: formState.region[1] ?? 0,
|
|
|
+ width: formState.region[2] ?? 50,
|
|
|
+ height: formState.region[3] ?? 50,
|
|
|
+ },
|
|
|
+ ]
|
|
|
+}
|
|
|
+initBlocks()
|
|
|
+
|
|
|
+const thresholdTimeFormat = ref<'s' | 'min' | 'hour' | 'day'>('s') // 触发阈值 额外选择器
|
|
|
+const timeThresholdFormat = ref<'s' | 'min' | 'hour' | 'day'>('s') // 异常消失时间阈值 额外选择器
|
|
|
+
|
|
|
+const plainOptions = ref<(number | string)[]>(weekOptions)
|
|
|
const checkState = reactive({
|
|
|
indeterminate: true,
|
|
|
checkAll: false,
|
|
|
@@ -215,10 +518,31 @@ const onCheckAllChange = (e: Event) => {
|
|
|
formState.effectTimeRanges = checked ? [...plainOptions.value] : []
|
|
|
}
|
|
|
|
|
|
+// 星期映射数字
|
|
|
+const weekToNumMap: Record<string, number> = {
|
|
|
+ 周一: 1,
|
|
|
+ 周二: 2,
|
|
|
+ 周三: 3,
|
|
|
+ 周四: 4,
|
|
|
+ 周五: 5,
|
|
|
+ 周六: 6,
|
|
|
+ 周日: 7,
|
|
|
+}
|
|
|
+// 数字映射星期
|
|
|
+const numToWeekMap: Record<number, string> = {
|
|
|
+ 1: '周一',
|
|
|
+ 2: '周二',
|
|
|
+ 3: '周三',
|
|
|
+ 4: '周四',
|
|
|
+ 5: '周五',
|
|
|
+ 6: '周六',
|
|
|
+ 7: '周日',
|
|
|
+}
|
|
|
+
|
|
|
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 // 设置全选按钮的选中状态
|
|
|
},
|
|
|
{
|
|
|
@@ -226,6 +550,104 @@ watch(
|
|
|
}
|
|
|
)
|
|
|
|
|
|
+const safeParse = <T,>(str: string | undefined | null, fallback: T): T => {
|
|
|
+ try {
|
|
|
+ return JSON.parse(str ?? JSON.stringify(fallback)) as T
|
|
|
+ } catch {
|
|
|
+ return fallback
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+interface ParamType {
|
|
|
+ start_time?: string
|
|
|
+ end_time?: string
|
|
|
+ count?: number
|
|
|
+ time_threshold?: number
|
|
|
+}
|
|
|
+
|
|
|
+interface TimeFrame {
|
|
|
+ start_time?: string
|
|
|
+ end_time?: string
|
|
|
+}
|
|
|
+
|
|
|
+interface AlarmTimePlan {
|
|
|
+ startDate?: string
|
|
|
+ stopDate?: string
|
|
|
+ weekdays?: string
|
|
|
+ monthDays?: string
|
|
|
+ timeRange?: string
|
|
|
+}
|
|
|
+
|
|
|
+interface SourceData {
|
|
|
+ name?: string
|
|
|
+ remark?: string
|
|
|
+ thresholdTime?: number
|
|
|
+ mergeTime?: number
|
|
|
+ eventVal?: number | null
|
|
|
+ param?: string
|
|
|
+ alarmTimePlan?: AlarmTimePlan
|
|
|
+ region?: string
|
|
|
+ enable?: number
|
|
|
+}
|
|
|
+
|
|
|
+const echoFormState = (val: SourceData) => {
|
|
|
+ const paramObj = safeParse<ParamType>(val.param, {})
|
|
|
+ const weekdays = safeParse<string[]>(val.alarmTimePlan?.weekdays, [])
|
|
|
+ const monthDays = safeParse<string[]>(val.alarmTimePlan?.monthDays, [])
|
|
|
+ const timeFrames = safeParse<TimeFrame[]>(val.alarmTimePlan?.timeRange, [])
|
|
|
+ plainOptions.value = weekdays.length === 0 ? monthOptions : weekOptions
|
|
|
+
|
|
|
+ return {
|
|
|
+ planName: val.name ?? '',
|
|
|
+ remark: val.remark ?? '',
|
|
|
+ thresholdTime: typeof val.thresholdTime === 'number' ? val.thresholdTime : null,
|
|
|
+ mergeTime: typeof val.mergeTime === 'number' ? val.mergeTime : null,
|
|
|
+ eventType: val.eventVal ?? null,
|
|
|
+ statisticsTime: [paramObj.start_time ?? null, paramObj.end_time ?? null] as string[],
|
|
|
+ count: paramObj.count ?? null,
|
|
|
+ timeThreshold: paramObj.time_threshold ?? null,
|
|
|
+ planTime: [val.alarmTimePlan?.startDate ?? '', val.alarmTimePlan?.stopDate ?? ''],
|
|
|
+ effectType: (weekdays.length === 0 ? 'month' : 'week') as 'week' | 'month',
|
|
|
+ effectTimeRanges:
|
|
|
+ weekdays.length === 0
|
|
|
+ ? monthDays
|
|
|
+ : weekdays.map((item: string) => numToWeekMap[Number(item)]),
|
|
|
+ effectTimeFrames: Array.isArray(timeFrames)
|
|
|
+ ? timeFrames.map((item) => ({
|
|
|
+ startTime: item?.start_time ?? '',
|
|
|
+ endTime: item?.end_time ?? '',
|
|
|
+ }))
|
|
|
+ : [],
|
|
|
+ region: JSON.parse(val.region ?? '[0, 0, 50, 50]'),
|
|
|
+ enable: val?.enable === 1,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.open,
|
|
|
+ (value) => {
|
|
|
+ const val = props.data as SourceData
|
|
|
+ console.log('🌸🌸 监听data用于编辑回显数据 🌸🌸', value, val)
|
|
|
+ if (value && val) {
|
|
|
+ formState.planName = echoFormState(val).planName
|
|
|
+ formState.remark = echoFormState(val).remark
|
|
|
+ formState.thresholdTime = echoFormState(val).thresholdTime
|
|
|
+ formState.mergeTime = echoFormState(val).mergeTime
|
|
|
+ formState.eventType = echoFormState(val).eventType
|
|
|
+ formState.statisticsTime = echoFormState(val).statisticsTime
|
|
|
+ formState.count = echoFormState(val).count
|
|
|
+ formState.timeThreshold = echoFormState(val).timeThreshold
|
|
|
+ formState.planTime = echoFormState(val).planTime
|
|
|
+ formState.effectType = echoFormState(val).effectType
|
|
|
+ formState.effectTimeRanges = echoFormState(val).effectTimeRanges
|
|
|
+ formState.effectTimeFrames = echoFormState(val).effectTimeFrames
|
|
|
+ formState.region = echoFormState(val).region
|
|
|
+ initBlocks()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
+
|
|
|
// 生效方式变化 周week、月month
|
|
|
const effectTypeChange = (e: Event) => {
|
|
|
const value = (e.target as HTMLInputElement).value
|
|
|
@@ -234,9 +656,9 @@ const effectTypeChange = (e: Event) => {
|
|
|
if (value === 'week') {
|
|
|
plainOptions.value = weekOptions
|
|
|
formState.effectTimeRanges = weekOptions
|
|
|
- } else if (value === 'mouth') {
|
|
|
- plainOptions.value = mouthOptions
|
|
|
- formState.effectTimeRanges = mouthOptions
|
|
|
+ } else if (value === 'month') {
|
|
|
+ plainOptions.value = monthOptions
|
|
|
+ formState.effectTimeRanges = monthOptions
|
|
|
}
|
|
|
/* 置全选按钮的状态 */
|
|
|
checkState.indeterminate =
|
|
|
@@ -264,9 +686,71 @@ const deleteEffectTimeItem = (e: Event, index: number) => {
|
|
|
formState.effectTimeFrames.splice(index, 1)
|
|
|
}
|
|
|
|
|
|
+// 关闭弹窗
|
|
|
const cancel = () => {
|
|
|
formRef?.value?.resetFields()
|
|
|
emit('update:open', false)
|
|
|
+ // 重置表单
|
|
|
+ formState.planName = ''
|
|
|
+ formState.region = [0, 0, 50, 50]
|
|
|
+ formState.eventType = null
|
|
|
+ formState.thresholdTime = 300
|
|
|
+ formState.mergeTime = 30
|
|
|
+ formState.planTime = []
|
|
|
+ formState.effectType = 'week'
|
|
|
+ plainOptions.value = weekOptions
|
|
|
+ formState.effectTimeRanges = weekOptions
|
|
|
+ formState.effectTimeFrame = []
|
|
|
+ formState.effectTimeFrames = []
|
|
|
+ formState.enable = true
|
|
|
+ formState.statisticsTime = []
|
|
|
+ formState.count = 3
|
|
|
+ formState.timeThreshold = 300
|
|
|
+ formState.remark = ''
|
|
|
+}
|
|
|
+
|
|
|
+const eventTypeList = ref<{ label: string; value: string }[]>([])
|
|
|
+// 获取事件类型下拉列表
|
|
|
+const fetchEventTypeList = async () => {
|
|
|
+ try {
|
|
|
+ const res = await alarmApi.getAlarmEventTypeList()
|
|
|
+ console.log('获取事件类型下拉列表成功✅', res)
|
|
|
+ const data = res.data
|
|
|
+ eventTypeList.value =
|
|
|
+ (Array.isArray(data) &&
|
|
|
+ data.map((item) => ({
|
|
|
+ label: item.eventDesc,
|
|
|
+ value: item.eventVal,
|
|
|
+ }))) ||
|
|
|
+ []
|
|
|
+ } catch (err) {
|
|
|
+ console.log('获取事件类型下拉列表失败❌', err)
|
|
|
+ }
|
|
|
+}
|
|
|
+fetchEventTypeList()
|
|
|
+
|
|
|
+function thresholdTimeFormatValue() {
|
|
|
+ if (thresholdTimeFormat.value === 's') {
|
|
|
+ return Number(formState.thresholdTime) // 触发阈值
|
|
|
+ } else if (thresholdTimeFormat.value === 'min') {
|
|
|
+ return Number(formState.thresholdTime) * 60 // 触发阈值
|
|
|
+ } else if (thresholdTimeFormat.value === 'hour') {
|
|
|
+ return Number(formState.thresholdTime) * 60 * 60 // 触发阈值
|
|
|
+ } else if (thresholdTimeFormat.value === 'day') {
|
|
|
+ return Number(formState.thresholdTime) * 24 * 60 * 60 // 触发阈值
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function timeThresholdFormatValue() {
|
|
|
+ if (timeThresholdFormat.value === 's') {
|
|
|
+ return Number(formState.timeThreshold) // 触发阈值
|
|
|
+ } else if (timeThresholdFormat.value === 'min') {
|
|
|
+ return Number(formState.timeThreshold) * 60 // 触发阈值
|
|
|
+ } else if (timeThresholdFormat.value === 'hour') {
|
|
|
+ return Number(formState.timeThreshold) * 60 * 60 // 触发阈值
|
|
|
+ } else if (timeThresholdFormat.value === 'day') {
|
|
|
+ return Number(formState.timeThreshold) * 24 * 60 * 60 // 触发阈值
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
const submitLoading = ref(false)
|
|
|
@@ -276,24 +760,92 @@ const submit = () => {
|
|
|
?.validate()
|
|
|
.then(() => {
|
|
|
console.log('校验通过', formState)
|
|
|
- // submitLoading.value = true
|
|
|
- // deviceApi
|
|
|
- // .addDevice({
|
|
|
- // clientId: formState.deviceId,
|
|
|
- // devType: formState.deviceType,
|
|
|
- // software: formState.firmwareVersion,
|
|
|
- // tenantId: formState.tenantId,
|
|
|
- // })
|
|
|
- // .then((res) => {
|
|
|
- // console.log('添加成功', res)
|
|
|
- // submitLoading.value = false
|
|
|
- // message.success('添加成功')
|
|
|
- // emit('success')
|
|
|
- // cancel()
|
|
|
- // })
|
|
|
- // .catch(() => {
|
|
|
- // submitLoading.value = false
|
|
|
- // })
|
|
|
+ let paramData = {}
|
|
|
+ if ([1, 2, 3].includes(formState.eventType as number)) {
|
|
|
+ paramData = {}
|
|
|
+ console.log('🔥paramData🔥', paramData)
|
|
|
+ } else if ([4, 5, 8].includes(formState.eventType as number)) {
|
|
|
+ paramData = {
|
|
|
+ start_time: formState.statisticsTime[0],
|
|
|
+ end_time: formState.statisticsTime[1],
|
|
|
+ }
|
|
|
+ console.log('🔥paramData🔥', paramData)
|
|
|
+ } else if ([6, 7].includes(formState.eventType as number)) {
|
|
|
+ paramData = {
|
|
|
+ start_time: formState.statisticsTime[0],
|
|
|
+ end_time: formState.statisticsTime[1],
|
|
|
+ count: isNaN(Number(formState.count)) ? 0 : Number(formState.count),
|
|
|
+ }
|
|
|
+ console.log('🔥paramData🔥', paramData)
|
|
|
+ } else if ([9].includes(formState.eventType as number)) {
|
|
|
+ paramData = {
|
|
|
+ time_threshold: isNaN(Number(formState.timeThreshold)) ? 0 : timeThresholdFormatValue(),
|
|
|
+ }
|
|
|
+ console.log('🔥paramData🔥', paramData)
|
|
|
+ }
|
|
|
+
|
|
|
+ const params = {
|
|
|
+ alarmPlanId: props.alarmPlanId || undefined, // 告警计划ID 编辑时传入
|
|
|
+ clientId: props.clientId, // 设备ID
|
|
|
+ name: formState.planName, // 计划名称
|
|
|
+ remark: formState.remark || '', // 备注
|
|
|
+ thresholdTime: thresholdTimeFormatValue(), // 触发阈值
|
|
|
+ mergeTime: Number(formState.mergeTime) || 30, // 归并时间
|
|
|
+ eventVal: formState.eventType as number, // 事件类型 与 param 有联动关系
|
|
|
+ param: JSON.stringify(paramData), // 事件参数 与 eventVal 有联动关系
|
|
|
+ region: [1, 2, 3, 9].includes(formState.eventType as number)
|
|
|
+ ? JSON.stringify(formState.region)
|
|
|
+ : [], // 检测区域
|
|
|
+ enable: Number(formState.enable) as 0 | 1, // 是否启用 0否 1是
|
|
|
+
|
|
|
+ // 生效方式
|
|
|
+ alarmTimePlan: {
|
|
|
+ startDate: formState.planTime[0], // 计划开始时间
|
|
|
+ stopDate: formState.planTime[1], // 计划结束时间
|
|
|
+ // 生效时段
|
|
|
+ timeRange: JSON.stringify(
|
|
|
+ formState.effectTimeFrames.map((item) => ({
|
|
|
+ start_time: item.startTime,
|
|
|
+ end_time: item.endTime,
|
|
|
+ }))
|
|
|
+ ),
|
|
|
+ monthDays: JSON.stringify(
|
|
|
+ formState.effectType === 'month' ? formState.effectTimeRanges : []
|
|
|
+ ),
|
|
|
+ weekdays: JSON.stringify(
|
|
|
+ formState.effectType === 'week'
|
|
|
+ ? formState.effectTimeRanges.map((item) => weekToNumMap[item])
|
|
|
+ : []
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ }
|
|
|
+ console.log('🚀🚀🚀提交参数', params)
|
|
|
+ if (formState.effectTimeFrames.length === 0) {
|
|
|
+ message.warn('请添加生效时段')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (formState.effectTimeRanges.length === 0) {
|
|
|
+ message.warn('请选择生效方式的范围')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // if ([1, 2, 3, 9].includes(formState.eventType as number) && formState.region.length !== 4) {
|
|
|
+ // message.warn('请选择检测区域')
|
|
|
+ // return
|
|
|
+ // }
|
|
|
+
|
|
|
+ submitLoading.value = true
|
|
|
+ alarmApi
|
|
|
+ .saveAlarmPlan(params)
|
|
|
+ .then((res) => {
|
|
|
+ console.log('添加成功', res)
|
|
|
+ submitLoading.value = false
|
|
|
+ message.success('添加成功')
|
|
|
+ emit('success')
|
|
|
+ cancel()
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ submitLoading.value = false
|
|
|
+ })
|
|
|
})
|
|
|
.catch((err) => {
|
|
|
console.log('校验失败', err)
|
|
|
@@ -320,4 +872,103 @@ const submit = () => {
|
|
|
:deep(.ant-tag) {
|
|
|
margin-inline-end: 0 !important;
|
|
|
}
|
|
|
+
|
|
|
+.eventTypeBox {
|
|
|
+ margin-top: 12px;
|
|
|
+ color: #555;
|
|
|
+ font-size: 14px;
|
|
|
+ padding: 20px 12px;
|
|
|
+ background: #fafafa;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
|
|
+ &-item {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+
|
|
|
+ &-label {
|
|
|
+ width: 100px;
|
|
|
+ }
|
|
|
+ &-labelend {
|
|
|
+ width: 140px;
|
|
|
+ }
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ :deep(.ant-form-item) {
|
|
|
+ flex: 1;
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.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>
|