index.vue 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974
  1. <template>
  2. <div ref="mod">
  3. <a-modal
  4. :get-container="() => $refs.mod"
  5. :open="props.open"
  6. :title="modelTitle"
  7. :mask-closable="false"
  8. width="600px"
  9. @cancel="cancel"
  10. :footer="null"
  11. >
  12. <a-form
  13. ref="formRef"
  14. :model="formState"
  15. :label-col="{ style: { width: '80px' } }"
  16. hideRequiredMark
  17. >
  18. <a-form-item
  19. label="计划名称"
  20. name="planName"
  21. :rules="[{ required: true, message: '请输入计划名称' }]"
  22. >
  23. <a-input
  24. v-model:value.trim="formState.planName"
  25. placeholder="请输入计划名称"
  26. :maxlength="20"
  27. show-count
  28. allow-clear
  29. />
  30. </a-form-item>
  31. <a-form-item
  32. label="计划备注"
  33. name="planName"
  34. :rules="[{ required: true, message: '请输入计划备注' }]"
  35. >
  36. <a-input
  37. v-model:value.trim="formState.remark"
  38. placeholder="请输入备注"
  39. :maxlength="200"
  40. show-count
  41. allow-clear
  42. />
  43. </a-form-item>
  44. <a-form-item
  45. label="触发阈值"
  46. name="thresholdTime"
  47. :rules="[{ required: true, message: '请输入触发阈值' }]"
  48. >
  49. <a-input-number
  50. v-model:value.trim="formState.thresholdTime"
  51. placeholder="请输入(默认300,需要大于0)"
  52. min="0"
  53. show-count
  54. allow-clear
  55. style="width: 100%"
  56. >
  57. <template #addonAfter>
  58. <a-select v-model:value="thresholdTimeFormat" style="width: 80px">
  59. <a-select-option value="s">秒</a-select-option>
  60. <a-select-option value="min">分钟</a-select-option>
  61. <a-select-option value="hour">小时</a-select-option>
  62. <a-select-option value="day">天</a-select-option>
  63. </a-select>
  64. </template>
  65. </a-input-number>
  66. </a-form-item>
  67. <a-form-item
  68. label="归并时间"
  69. name="mergeTime"
  70. :rules="[{ required: true, message: '请输入归并时间' }]"
  71. >
  72. <a-input-number
  73. v-model:value.trim="formState.mergeTime"
  74. placeholder="请输入(默认30,需要大于0)"
  75. min="0"
  76. show-count
  77. allow-clear
  78. style="width: 100%"
  79. />
  80. </a-form-item>
  81. <a-form-item
  82. label="事件类型"
  83. name="eventType"
  84. :rules="[{ required: true, message: '请选择事件类型' }]"
  85. >
  86. <a-select
  87. v-model:value="formState.eventType"
  88. :options="eventTypeList"
  89. placeholder="请选择事件类型"
  90. />
  91. <a-form-item-rest v-if="formState.eventType && ![1, 2, 3].includes(formState.eventType)">
  92. <div class="eventTypeBox">
  93. <div v-if="[4, 5, 6, 7, 8].includes(formState.eventType)" class="eventTypeBox-item">
  94. <span class="eventTypeBox-item-label">统计时间:</span>
  95. <a-form-item
  96. name="statisticsTime"
  97. :rules="[{ required: true, message: '请选择统计时间' }]"
  98. >
  99. <a-time-range-picker
  100. v-model:value="formState.statisticsTime"
  101. valueFormat="HH:mm"
  102. format="HH:mm"
  103. style="width: 100%"
  104. />
  105. </a-form-item>
  106. </div>
  107. <div v-if="[6, 7].includes(formState.eventType)" class="eventTypeBox-item">
  108. <span class="eventTypeBox-item-label">异常阈值:</span>
  109. <a-form-item name="count" :rules="[{ required: true, message: '请输入异常阈值' }]">
  110. <a-input-number
  111. v-model:value.trim="formState.count"
  112. placeholder="请输入(默认3,需要大于0)"
  113. min="0"
  114. show-count
  115. allow-clear
  116. style="width: 100%"
  117. />
  118. </a-form-item>
  119. </div>
  120. <div v-if="formState.eventType === 9" class="eventTypeBox-item">
  121. <span class="eventTypeBox-item-labelend">异常消失时间阈值:</span>
  122. <a-form-item
  123. name="timeThreshold"
  124. :rules="[{ required: true, message: '请输入异常消失时间阈值' }]"
  125. >
  126. <a-input-number
  127. v-model:value.trim="formState.timeThreshold"
  128. placeholder="请输入(默认300,需要大于0)"
  129. min="0"
  130. show-count
  131. allow-clear
  132. style="width: 100%"
  133. >
  134. <template #addonAfter>
  135. <a-select v-model:value="timeThresholdFormat" style="width: 80px">
  136. <a-select-option value="s">秒</a-select-option>
  137. <a-select-option value="min">分钟</a-select-option>
  138. <a-select-option value="hour">小时</a-select-option>
  139. <a-select-option value="day">天</a-select-option>
  140. </a-select>
  141. </template>
  142. </a-input-number>
  143. </a-form-item>
  144. </div>
  145. </div>
  146. </a-form-item-rest>
  147. </a-form-item>
  148. <a-form-item
  149. label="计划时间"
  150. name="planTime"
  151. :rules="[{ type: 'array' as const, required: true, message: '请选择计划时间' }]"
  152. >
  153. <a-range-picker
  154. v-model:value="formState.planTime"
  155. style="width: 100%"
  156. :show-time="false"
  157. valueFormat="YYYY-MM-DD"
  158. />
  159. </a-form-item>
  160. <a-form-item label="生效时段">
  161. <div style="display: flex; align-items: center; gap: 8px">
  162. <a-time-range-picker
  163. v-model:value="formState.effectTimeFrame"
  164. valueFormat="HH:mm"
  165. format="HH:mm"
  166. style="width: 100%"
  167. />
  168. <a-button size="small" type="link" @click="addEffectTime">添加</a-button>
  169. </div>
  170. <div style="margin-top: 12px">
  171. <span
  172. v-if="formState.effectTimeFrames && !formState.effectTimeFrames.length"
  173. style="color: #aaa; font-size: 14px"
  174. >⚠️暂无生效时段</span
  175. >
  176. <a-space wrap v-else>
  177. <a-tag
  178. v-for="(item, index) in formState.effectTimeFrames"
  179. :key="index"
  180. closable
  181. style="font-size: 14px; padding: 4px 10px"
  182. @close="deleteEffectTimeItem($event, index)"
  183. >{{ item.startTime }} - {{ item.endTime }}</a-tag
  184. >
  185. </a-space>
  186. </div>
  187. </a-form-item>
  188. <a-form-item label="生效方式">
  189. <a-form-item-rest>
  190. <a-radio-group
  191. v-model:value="formState.effectType"
  192. name="radioGroup"
  193. @change="effectTypeChange"
  194. >
  195. <a-radio value="week">按周</a-radio>
  196. <a-radio value="month">按月</a-radio>
  197. </a-radio-group>
  198. <a-checkbox
  199. v-model:checked="checkState.checkAll"
  200. :indeterminate="checkState.indeterminate"
  201. @change="onCheckAllChange"
  202. >
  203. 全选
  204. </a-checkbox>
  205. <a-checkbox-group v-model:value="formState.effectTimeRanges" :options="plainOptions" />
  206. </a-form-item-rest>
  207. </a-form-item>
  208. <a-form-item
  209. v-if="[1, 2, 3, 9].includes(formState?.eventType as number)"
  210. label="检测区域"
  211. name="region"
  212. style="user-select: none"
  213. >
  214. <span style="user-select: none">框选区域 {{ formState.region }}</span>
  215. <a-form-item-rest>
  216. <div class="viewer">
  217. <div class="viewer-content">
  218. <div
  219. class="mapBox blockArea"
  220. :style="{
  221. width: `${areaWidth}px`,
  222. height: `${areaHeight}px`,
  223. cursor: 'default',
  224. }"
  225. >
  226. <!-- 已创建区块 -->
  227. <div
  228. v-for="(block, blockIndex) in blocks"
  229. :key="blockIndex"
  230. class="block-item"
  231. :style="{
  232. left: `${block.x}px`,
  233. top: `${block.y}px`,
  234. width: `${block.width}px`,
  235. height: `${block.height}px`,
  236. border: `2px solid #1890ff`,
  237. position: 'absolute',
  238. cursor: 'move',
  239. backgroundColor: 'rgba(24, 144, 255, 0.1)',
  240. }"
  241. @mousedown="startDrag(block, $event)"
  242. @mousemove="drag(block)"
  243. @mouseup="endDrag(block)"
  244. >
  245. <div
  246. class="resize-handle"
  247. :style="{
  248. backgroundColor: '#1890ff',
  249. }"
  250. @mousedown.stop="startResize(block, $event)"
  251. >
  252. </div>
  253. </div>
  254. </div>
  255. </div>
  256. </div>
  257. </a-form-item-rest>
  258. </a-form-item>
  259. <a-form-item label="是否启用">
  260. <a-switch v-model:checked="formState.enable" />
  261. </a-form-item>
  262. </a-form>
  263. <div class="footer">
  264. <a-space>
  265. <a-button @click="cancel">取消</a-button>
  266. <a-button type="primary" :loading="submitLoading" @click="submit">保存</a-button>
  267. </a-space>
  268. </div>
  269. </a-modal>
  270. </div>
  271. </template>
  272. <script setup lang="ts">
  273. import { ref, reactive, watch, computed } from 'vue'
  274. import { message, type FormInstance } from 'ant-design-vue'
  275. import * as alarmApi from '@/api/alarm'
  276. import { getOriginPosition } from '@/utils/index'
  277. defineOptions({
  278. name: 'AlarmPlanModal',
  279. })
  280. const formRef = ref<FormInstance>()
  281. type AlarmPlan = {
  282. id: number
  283. uuid: ID
  284. name: string
  285. clientId: string
  286. enable: SwitchType
  287. region: string
  288. eventVal: number
  289. alarmTimePlanId: ID
  290. thresholdTime: ID
  291. mergeTime: ID
  292. param: string
  293. createTime: string
  294. updateTime: string
  295. remark: string | null
  296. alarmTimePlan: {
  297. createId: ID
  298. updateId: ID
  299. createTime: ID
  300. updateTime: ID
  301. isDeleted: SwitchType | null
  302. remark: ID
  303. id: ID
  304. startDate: string
  305. stopDate: string
  306. timeRange: string
  307. monthDays: string
  308. weekdays: string
  309. }
  310. }
  311. type Props = {
  312. open: boolean
  313. title?: string
  314. clientId: string // 设备ID
  315. alarmPlanId?: number | null // 告警计划ID 编辑时传入
  316. data?: AlarmPlan // 编辑数据
  317. area?: {
  318. width: number
  319. height: number
  320. ranges: number[]
  321. }
  322. }
  323. const emit = defineEmits<{
  324. (e: 'update:open', value: boolean): void
  325. (e: 'success', value: void): void
  326. }>()
  327. const props = withDefaults(defineProps<Props>(), {
  328. open: false,
  329. title: '告警计划',
  330. clientId: '',
  331. alarmPlanId: null,
  332. data: undefined,
  333. })
  334. const modelTitle = computed(() => {
  335. return props.alarmPlanId ? '编辑告警计划' : '新增告警计划'
  336. })
  337. // 检测区域宽度
  338. const areaWidth = computed(() => {
  339. return Math.abs(props.area?.width || 0)
  340. })
  341. // 检测区域高度
  342. const areaHeight = computed(() => {
  343. return Math.abs(props.area?.height || 0)
  344. })
  345. // 获取原点坐标
  346. const { originX, originY } = getOriginPosition(props.area!.ranges, [0, 0])
  347. interface BlockItem {
  348. // 本地用
  349. x: number // 区块基于父元素的X偏移量,区块的左上角x坐标
  350. y: number // 区块基于父元素的Y偏移量,区块的左上角y坐标
  351. ox: number // 区块基于原点的X偏移量,区块的左上角x坐标
  352. oy: number // 区块基于原点的Y偏移量,区块的左上角y坐标
  353. width: number // 区块宽度
  354. height: number // 区块高度
  355. }
  356. const blocks = ref<BlockItem[]>([])
  357. // 区块拖动
  358. const startDrag = (block: BlockItem, e: MouseEvent) => {
  359. console.log('startDrag', block)
  360. e.stopPropagation()
  361. const container = document.querySelector('.blockArea') as HTMLElement
  362. const rect = container.getBoundingClientRect()
  363. const offsetX = e.clientX - rect.left - block.x
  364. const offsetY = e.clientY - rect.top - block.y
  365. const moveHandler = (e: MouseEvent) => {
  366. const newX = e.clientX - rect.left - offsetX
  367. const newY = e.clientY - rect.top - offsetY
  368. const containerWidth = container.offsetWidth
  369. const containerHeight = container.offsetHeight
  370. block.x = Math.max(0, Math.min(newX, containerWidth - block.width))
  371. block.y = Math.max(0, Math.min(newY, containerHeight - block.height))
  372. block.ox = block.x - originX
  373. block.oy = originY - block.y
  374. }
  375. const upHandler = () => {
  376. document.removeEventListener('mousemove', moveHandler)
  377. document.removeEventListener('mouseup', upHandler)
  378. }
  379. document.addEventListener('mousemove', moveHandler)
  380. document.addEventListener('mouseup', upHandler)
  381. }
  382. const drag = (block: BlockItem) => {
  383. formState.region = [block.ox, block.oy, block.width, block.height]
  384. }
  385. const endDrag = (block: BlockItem) => {
  386. formState.region = [block.ox, block.oy, block.width, block.height]
  387. }
  388. // 获取容器边界
  389. const getContainerRect = () => {
  390. const container = document.querySelector('.blockArea') as HTMLElement
  391. return container?.getBoundingClientRect() || { left: 0, top: 0 }
  392. }
  393. const startResize = (block: BlockItem, e: MouseEvent) => {
  394. const startX = e.clientX
  395. const startY = e.clientY
  396. const initialWidth = block.width
  397. const initialHeight = block.height
  398. const moveHandler = (e: MouseEvent) => {
  399. const rect = getContainerRect()
  400. const deltaX = e.clientX - startX
  401. const deltaY = e.clientY - startY
  402. // 限制最小尺寸和容器边界
  403. block.width = Math.max(0, Math.min(initialWidth + deltaX, rect.width - block.x))
  404. block.height = Math.max(0, Math.min(initialHeight + deltaY, rect.height - block.y))
  405. }
  406. const upHandler = () => {
  407. document.removeEventListener('mousemove', moveHandler)
  408. document.removeEventListener('mouseup', upHandler)
  409. }
  410. document.addEventListener('mousemove', moveHandler)
  411. document.addEventListener('mouseup', upHandler)
  412. }
  413. const weekOptions = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
  414. const monthOptions = [
  415. 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,
  416. 28, 29, 30, 31,
  417. ]
  418. type FormState = {
  419. planName: string // 计划名称
  420. region: number[] // 检测区域 [left, top, width, height]
  421. eventType: number | null // 事件类型
  422. thresholdTime: number | null // 触发阈值
  423. mergeTime: number | null // 归并时间
  424. planTime: string[] // 计划时间
  425. effectType: 'week' | 'month' // 生效方式 周week、月month
  426. effectTimeRanges: (number | string)[] // 生效范围 周 1-7、月 1-31
  427. effectTimeFrame: string[] // 生效时段 单条 00:00:00 - 23:59:59
  428. effectTimeFrames: { startTime: string; endTime: string }[] // 生效时段 多条
  429. enable: boolean // 是否启用
  430. statisticsTime: string[] // 统计时间
  431. count?: number | null // 异常阈值
  432. timeThreshold?: number | null // 异常消失时间阈值(单位:秒)
  433. remark?: string // 备注
  434. }
  435. const formState = reactive<FormState>({
  436. planName: '',
  437. region: [0, 0, 50, 50],
  438. eventType: null,
  439. thresholdTime: 300,
  440. mergeTime: 30,
  441. planTime: [],
  442. effectType: 'week',
  443. effectTimeRanges: weekOptions,
  444. effectTimeFrame: [],
  445. effectTimeFrames: [],
  446. enable: true,
  447. statisticsTime: [],
  448. count: 3,
  449. timeThreshold: 300,
  450. })
  451. const initBlocks = () => {
  452. blocks.value = [
  453. {
  454. x: 0,
  455. y: 0,
  456. ox: formState.region[0] ?? 0,
  457. oy: formState.region[1] ?? 0,
  458. width: formState.region[2] ?? 50,
  459. height: formState.region[3] ?? 50,
  460. },
  461. ]
  462. }
  463. initBlocks()
  464. const thresholdTimeFormat = ref<'s' | 'min' | 'hour' | 'day'>('s') // 触发阈值 额外选择器
  465. const timeThresholdFormat = ref<'s' | 'min' | 'hour' | 'day'>('s') // 异常消失时间阈值 额外选择器
  466. const plainOptions = ref<(number | string)[]>(weekOptions)
  467. const checkState = reactive({
  468. indeterminate: true,
  469. checkAll: false,
  470. })
  471. const onCheckAllChange = (e: Event) => {
  472. const checked = (e.target as HTMLInputElement).checked
  473. checkState.checkAll = checked
  474. console.log('onCheckAllChange', e, checked)
  475. formState.effectTimeRanges = checked ? [...plainOptions.value] : []
  476. }
  477. // 星期映射数字
  478. const weekToNumMap: Record<string, number> = {
  479. 周一: 1,
  480. 周二: 2,
  481. 周三: 3,
  482. 周四: 4,
  483. 周五: 5,
  484. 周六: 6,
  485. 周日: 7,
  486. }
  487. // 数字映射星期
  488. const numToWeekMap: Record<number, string> = {
  489. 1: '周一',
  490. 2: '周二',
  491. 3: '周三',
  492. 4: '周四',
  493. 5: '周五',
  494. 6: '周六',
  495. 7: '周日',
  496. }
  497. watch(
  498. () => formState.effectTimeRanges,
  499. (val) => {
  500. checkState.indeterminate = !!val.length && val.length < plainOptions.value.length // 设置全选按钮的半选状态
  501. checkState.checkAll = val.length === plainOptions.value.length // 设置全选按钮的选中状态
  502. },
  503. {
  504. immediate: true,
  505. }
  506. )
  507. const safeParse = <T,>(str: string | undefined | null, fallback: T): T => {
  508. try {
  509. return JSON.parse(str ?? JSON.stringify(fallback)) as T
  510. } catch {
  511. return fallback
  512. }
  513. }
  514. interface ParamType {
  515. start_time?: string
  516. end_time?: string
  517. count?: number
  518. time_threshold?: number
  519. }
  520. interface TimeFrame {
  521. start_time?: string
  522. end_time?: string
  523. }
  524. interface AlarmTimePlan {
  525. startDate?: string
  526. stopDate?: string
  527. weekdays?: string
  528. monthDays?: string
  529. timeRange?: string
  530. }
  531. interface SourceData {
  532. name?: string
  533. remark?: string
  534. thresholdTime?: number
  535. mergeTime?: number
  536. eventVal?: number | null
  537. param?: string
  538. alarmTimePlan?: AlarmTimePlan
  539. region?: string
  540. enable?: number
  541. }
  542. const echoFormState = (val: SourceData) => {
  543. const paramObj = safeParse<ParamType>(val.param, {})
  544. const weekdays = safeParse<string[]>(val.alarmTimePlan?.weekdays, [])
  545. const monthDays = safeParse<string[]>(val.alarmTimePlan?.monthDays, [])
  546. const timeFrames = safeParse<TimeFrame[]>(val.alarmTimePlan?.timeRange, [])
  547. plainOptions.value = weekdays.length === 0 ? monthOptions : weekOptions
  548. return {
  549. planName: val.name ?? '',
  550. remark: val.remark ?? '',
  551. thresholdTime: typeof val.thresholdTime === 'number' ? val.thresholdTime : null,
  552. mergeTime: typeof val.mergeTime === 'number' ? val.mergeTime : null,
  553. eventType: val.eventVal ?? null,
  554. statisticsTime: [paramObj.start_time ?? null, paramObj.end_time ?? null] as string[],
  555. count: paramObj.count ?? null,
  556. timeThreshold: paramObj.time_threshold ?? null,
  557. planTime: [val.alarmTimePlan?.startDate ?? '', val.alarmTimePlan?.stopDate ?? ''],
  558. effectType: (weekdays.length === 0 ? 'month' : 'week') as 'week' | 'month',
  559. effectTimeRanges:
  560. weekdays.length === 0
  561. ? monthDays
  562. : weekdays.map((item: string) => numToWeekMap[Number(item)]),
  563. effectTimeFrames: Array.isArray(timeFrames)
  564. ? timeFrames.map((item) => ({
  565. startTime: item?.start_time ?? '',
  566. endTime: item?.end_time ?? '',
  567. }))
  568. : [],
  569. region: JSON.parse(val.region ?? '[0, 0, 50, 50]'),
  570. enable: val?.enable === 1,
  571. }
  572. }
  573. watch(
  574. () => props.open,
  575. (value) => {
  576. const val = props.data as SourceData
  577. console.log('🌸🌸 监听data用于编辑回显数据 🌸🌸', value, val)
  578. if (value && val) {
  579. formState.planName = echoFormState(val).planName
  580. formState.remark = echoFormState(val).remark
  581. formState.thresholdTime = echoFormState(val).thresholdTime
  582. formState.mergeTime = echoFormState(val).mergeTime
  583. formState.eventType = echoFormState(val).eventType
  584. formState.statisticsTime = echoFormState(val).statisticsTime
  585. formState.count = echoFormState(val).count
  586. formState.timeThreshold = echoFormState(val).timeThreshold
  587. formState.planTime = echoFormState(val).planTime
  588. formState.effectType = echoFormState(val).effectType
  589. formState.effectTimeRanges = echoFormState(val).effectTimeRanges
  590. formState.effectTimeFrames = echoFormState(val).effectTimeFrames
  591. formState.region = echoFormState(val).region
  592. initBlocks()
  593. }
  594. },
  595. { immediate: true }
  596. )
  597. // 生效方式变化 周week、月month
  598. const effectTypeChange = (e: Event) => {
  599. const value = (e.target as HTMLInputElement).value
  600. console.log('effectTypeChange', e, value)
  601. if (value === 'week') {
  602. plainOptions.value = weekOptions
  603. formState.effectTimeRanges = weekOptions
  604. } else if (value === 'month') {
  605. plainOptions.value = monthOptions
  606. formState.effectTimeRanges = monthOptions
  607. }
  608. /* 置全选按钮的状态 */
  609. checkState.indeterminate =
  610. !!formState.effectTimeRanges.length &&
  611. formState.effectTimeRanges.length < plainOptions.value.length // 设置全选按钮的半选状态
  612. checkState.checkAll = formState.effectTimeRanges.length === plainOptions.value.length // 设置全选按钮的选中状态
  613. }
  614. // 添加时间段
  615. const addEffectTime = () => {
  616. console.log('addEffectTime', formState.effectTimeFrame)
  617. if (!formState.effectTimeFrame || !formState.effectTimeFrame.length) {
  618. message.warn('请选择时间段')
  619. return
  620. }
  621. formState.effectTimeFrames.push({
  622. startTime: formState.effectTimeFrame[0],
  623. endTime: formState.effectTimeFrame[1],
  624. })
  625. // formState.effectTimeFrame = [] // 清空选择的时间段
  626. }
  627. // 删除已添加的时间段
  628. const deleteEffectTimeItem = (e: Event, index: number) => {
  629. console.log('deleteEffectTimeItem', e, index)
  630. formState.effectTimeFrames.splice(index, 1)
  631. }
  632. // 关闭弹窗
  633. const cancel = () => {
  634. formRef?.value?.resetFields()
  635. emit('update:open', false)
  636. // 重置表单
  637. formState.planName = ''
  638. formState.region = [0, 0, 50, 50]
  639. formState.eventType = null
  640. formState.thresholdTime = 300
  641. formState.mergeTime = 30
  642. formState.planTime = []
  643. formState.effectType = 'week'
  644. plainOptions.value = weekOptions
  645. formState.effectTimeRanges = weekOptions
  646. formState.effectTimeFrame = []
  647. formState.effectTimeFrames = []
  648. formState.enable = true
  649. formState.statisticsTime = []
  650. formState.count = 3
  651. formState.timeThreshold = 300
  652. formState.remark = ''
  653. }
  654. const eventTypeList = ref<{ label: string; value: string }[]>([])
  655. // 获取事件类型下拉列表
  656. const fetchEventTypeList = async () => {
  657. try {
  658. const res = await alarmApi.getAlarmEventTypeList()
  659. console.log('获取事件类型下拉列表成功✅', res)
  660. const data = res.data
  661. eventTypeList.value =
  662. (Array.isArray(data) &&
  663. data.map((item) => ({
  664. label: item.eventDesc,
  665. value: item.eventVal,
  666. }))) ||
  667. []
  668. } catch (err) {
  669. console.log('获取事件类型下拉列表失败❌', err)
  670. }
  671. }
  672. fetchEventTypeList()
  673. function thresholdTimeFormatValue() {
  674. if (thresholdTimeFormat.value === 's') {
  675. return Number(formState.thresholdTime) // 触发阈值
  676. } else if (thresholdTimeFormat.value === 'min') {
  677. return Number(formState.thresholdTime) * 60 // 触发阈值
  678. } else if (thresholdTimeFormat.value === 'hour') {
  679. return Number(formState.thresholdTime) * 60 * 60 // 触发阈值
  680. } else if (thresholdTimeFormat.value === 'day') {
  681. return Number(formState.thresholdTime) * 24 * 60 * 60 // 触发阈值
  682. }
  683. }
  684. function timeThresholdFormatValue() {
  685. if (timeThresholdFormat.value === 's') {
  686. return Number(formState.timeThreshold) // 触发阈值
  687. } else if (timeThresholdFormat.value === 'min') {
  688. return Number(formState.timeThreshold) * 60 // 触发阈值
  689. } else if (timeThresholdFormat.value === 'hour') {
  690. return Number(formState.timeThreshold) * 60 * 60 // 触发阈值
  691. } else if (timeThresholdFormat.value === 'day') {
  692. return Number(formState.timeThreshold) * 24 * 60 * 60 // 触发阈值
  693. }
  694. }
  695. const submitLoading = ref(false)
  696. // 确定
  697. const submit = () => {
  698. formRef?.value
  699. ?.validate()
  700. .then(() => {
  701. console.log('校验通过', formState)
  702. let paramData = {}
  703. if ([1, 2, 3].includes(formState.eventType as number)) {
  704. paramData = {}
  705. console.log('🔥paramData🔥', paramData)
  706. } else if ([4, 5, 8].includes(formState.eventType as number)) {
  707. paramData = {
  708. start_time: formState.statisticsTime[0],
  709. end_time: formState.statisticsTime[1],
  710. }
  711. console.log('🔥paramData🔥', paramData)
  712. } else if ([6, 7].includes(formState.eventType as number)) {
  713. paramData = {
  714. start_time: formState.statisticsTime[0],
  715. end_time: formState.statisticsTime[1],
  716. count: isNaN(Number(formState.count)) ? 0 : Number(formState.count),
  717. }
  718. console.log('🔥paramData🔥', paramData)
  719. } else if ([9].includes(formState.eventType as number)) {
  720. paramData = {
  721. time_threshold: isNaN(Number(formState.timeThreshold)) ? 0 : timeThresholdFormatValue(),
  722. }
  723. console.log('🔥paramData🔥', paramData)
  724. }
  725. const params = {
  726. alarmPlanId: props.alarmPlanId || undefined, // 告警计划ID 编辑时传入
  727. clientId: props.clientId, // 设备ID
  728. name: formState.planName, // 计划名称
  729. remark: formState.remark || '', // 备注
  730. thresholdTime: thresholdTimeFormatValue(), // 触发阈值
  731. mergeTime: Number(formState.mergeTime) || 30, // 归并时间
  732. eventVal: formState.eventType as number, // 事件类型 与 param 有联动关系
  733. param: JSON.stringify(paramData), // 事件参数 与 eventVal 有联动关系
  734. region: [1, 2, 3, 9].includes(formState.eventType as number)
  735. ? JSON.stringify(formState.region)
  736. : [], // 检测区域
  737. enable: Number(formState.enable) as 0 | 1, // 是否启用 0否 1是
  738. // 生效方式
  739. alarmTimePlan: {
  740. startDate: formState.planTime[0], // 计划开始时间
  741. stopDate: formState.planTime[1], // 计划结束时间
  742. // 生效时段
  743. timeRange: JSON.stringify(
  744. formState.effectTimeFrames.map((item) => ({
  745. start_time: item.startTime,
  746. end_time: item.endTime,
  747. }))
  748. ),
  749. monthDays: JSON.stringify(
  750. formState.effectType === 'month' ? formState.effectTimeRanges : []
  751. ),
  752. weekdays: JSON.stringify(
  753. formState.effectType === 'week'
  754. ? formState.effectTimeRanges.map((item) => weekToNumMap[item])
  755. : []
  756. ),
  757. },
  758. }
  759. console.log('🚀🚀🚀提交参数', params)
  760. if (formState.effectTimeFrames.length === 0) {
  761. message.warn('请添加生效时段')
  762. return
  763. }
  764. if (formState.effectTimeRanges.length === 0) {
  765. message.warn('请选择生效方式的范围')
  766. return
  767. }
  768. // if ([1, 2, 3, 9].includes(formState.eventType as number) && formState.region.length !== 4) {
  769. // message.warn('请选择检测区域')
  770. // return
  771. // }
  772. submitLoading.value = true
  773. alarmApi
  774. .saveAlarmPlan(params)
  775. .then((res) => {
  776. console.log('添加成功', res)
  777. submitLoading.value = false
  778. message.success('添加成功')
  779. emit('success')
  780. cancel()
  781. })
  782. .catch(() => {
  783. submitLoading.value = false
  784. })
  785. })
  786. .catch((err) => {
  787. console.log('校验失败', err)
  788. })
  789. }
  790. </script>
  791. <style scoped lang="less">
  792. :deep(.ant-modal) {
  793. .footer {
  794. text-align: right;
  795. }
  796. .ant-modal-body {
  797. padding: 12px 0;
  798. }
  799. }
  800. :deep(.ant-checkbox-group) {
  801. .ant-checkbox + span {
  802. min-width: 32px;
  803. }
  804. }
  805. :deep(.ant-tag) {
  806. margin-inline-end: 0 !important;
  807. }
  808. .eventTypeBox {
  809. margin-top: 12px;
  810. color: #555;
  811. font-size: 14px;
  812. padding: 20px 12px;
  813. background: #fafafa;
  814. border-radius: 8px;
  815. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
  816. &-item {
  817. margin-bottom: 12px;
  818. display: flex;
  819. align-items: center;
  820. gap: 8px;
  821. &-label {
  822. width: 100px;
  823. }
  824. &-labelend {
  825. width: 140px;
  826. }
  827. &:last-child {
  828. margin-bottom: 0;
  829. }
  830. :deep(.ant-form-item) {
  831. flex: 1;
  832. margin-bottom: 0;
  833. }
  834. }
  835. }
  836. .viewer {
  837. padding: 10px;
  838. min-width: 500px;
  839. flex-shrink: 0;
  840. &-header {
  841. display: flex;
  842. justify-content: space-between;
  843. padding-bottom: 20px;
  844. &-title {
  845. font-size: 16px;
  846. font-weight: 600;
  847. line-height: 24px;
  848. }
  849. &-subtitle {
  850. font-size: 14px;
  851. color: #666;
  852. }
  853. }
  854. &-content {
  855. display: flex;
  856. gap: 20px;
  857. }
  858. }
  859. .mapBox {
  860. background-color: #e0e0e0;
  861. background-image:
  862. linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
  863. linear-gradient(to right, rgba(0, 0, 0, 0.1) 1px, transparent 1px);
  864. background-size: 20px 20px;
  865. position: relative;
  866. flex-shrink: 0;
  867. // 添加黑边框
  868. &::before {
  869. content: '';
  870. position: absolute;
  871. top: -5px;
  872. left: -5px;
  873. width: calc(100% + 10px);
  874. height: calc(100% + 10px);
  875. border: 5px solid rgba(0, 0, 0, 0.8);
  876. box-sizing: border-box;
  877. pointer-events: none;
  878. }
  879. }
  880. .block-item {
  881. background: rgba(24, 144, 255, 0.1);
  882. .resize-handle {
  883. position: absolute;
  884. right: -4px;
  885. bottom: -4px;
  886. width: 15px;
  887. height: 15px;
  888. background: #1890ff;
  889. cursor: nwse-resize;
  890. font-size: 12px;
  891. color: #fff;
  892. display: flex;
  893. align-items: center;
  894. justify-content: center;
  895. }
  896. }
  897. </style>