index.vue 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083
  1. <template>
  2. <div ref="mod">
  3. <a-modal
  4. :get-container="() => $refs.mod"
  5. :open="props.open"
  6. :title="props.title"
  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="props.type === 'plan' && [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-checkbox v-model:checked="formState.linkagePushWechatService">
  261. 微信服务号推送
  262. </a-checkbox>
  263. </a-form-item>
  264. <a-form-item label="是否启用">
  265. <a-switch v-model:checked="formState.enable" />
  266. </a-form-item>
  267. </a-form>
  268. <div class="footer">
  269. <a-space>
  270. <a-button @click="cancel">取消</a-button>
  271. <a-button type="primary" :loading="submitLoading" @click="submit">保存</a-button>
  272. </a-space>
  273. </div>
  274. </a-modal>
  275. </div>
  276. </template>
  277. <script setup lang="ts">
  278. import { ref, reactive, watch, computed } from 'vue'
  279. import { message, type FormInstance } from 'ant-design-vue'
  280. import * as alarmApi from '@/api/alarm'
  281. import { getOriginPosition } from '@/utils/index'
  282. import type { AlarmPlanParams } from '@/api/alarm/types'
  283. import dayjs from 'dayjs'
  284. defineOptions({
  285. name: 'AlarmPlanModal',
  286. })
  287. const formRef = ref<FormInstance>()
  288. type AlarmPlan = {
  289. id: number
  290. uuid: ID
  291. name: string
  292. clientId: string
  293. enable: SwitchType
  294. region: string
  295. eventVal: number
  296. alarmTimePlanId: ID
  297. thresholdTime: ID
  298. mergeTime: ID
  299. param: string
  300. createTime: string
  301. updateTime: string
  302. remark: string | null
  303. alarmTimePlan: {
  304. createId: ID
  305. updateId: ID
  306. createTime: ID
  307. updateTime: ID
  308. isDeleted: SwitchType | null
  309. remark: ID
  310. id: ID
  311. startDate: string
  312. stopDate: string
  313. timeRange: string
  314. monthDays: string
  315. weekdays: string
  316. }
  317. }
  318. type Props = {
  319. open: boolean
  320. type?: 'plan' | 'template'
  321. title?: string
  322. clientId?: string // 设备ID
  323. alarmPlanId?: number | null // 告警计划ID 编辑时传入
  324. data?: AlarmPlan // 编辑数据
  325. area?: {
  326. width: number
  327. height: number
  328. ranges: number[]
  329. }
  330. }
  331. const emit = defineEmits<{
  332. (e: 'update:open', value: boolean): void
  333. (e: 'success', value: void): void
  334. }>()
  335. const props = withDefaults(defineProps<Props>(), {
  336. open: false,
  337. type: 'plan',
  338. title: '告警计划',
  339. clientId: '',
  340. alarmPlanId: null,
  341. data: undefined,
  342. })
  343. // const modelTitle = computed(() => {
  344. // return props.alarmPlanId ? '编辑告警计划' : '新增告警计划'
  345. // })
  346. // 检测区域宽度
  347. const areaWidth = computed(() => {
  348. return Math.abs(props.area?.width || 0)
  349. })
  350. // 检测区域高度
  351. const areaHeight = computed(() => {
  352. return Math.abs(props.area?.height || 0)
  353. })
  354. // 获取原点坐标
  355. const { originX, originY } = getOriginPosition(props.area!.ranges, [0, 0])
  356. interface BlockItem {
  357. // 本地用
  358. x: number // 区块基于父元素的X偏移量,区块的左上角x坐标
  359. y: number // 区块基于父元素的Y偏移量,区块的左上角y坐标
  360. ox: number // 区块基于原点的X偏移量,区块的左上角x坐标
  361. oy: number // 区块基于原点的Y偏移量,区块的左上角y坐标
  362. width: number // 区块宽度
  363. height: number // 区块高度
  364. }
  365. const blocks = ref<BlockItem[]>([])
  366. // 区块拖动
  367. const startDrag = (block: BlockItem, e: MouseEvent) => {
  368. console.log('startDrag', block)
  369. e.stopPropagation()
  370. const container = document.querySelector('.blockArea') as HTMLElement
  371. const rect = container.getBoundingClientRect()
  372. const offsetX = e.clientX - rect.left - block.x
  373. const offsetY = e.clientY - rect.top - block.y
  374. const moveHandler = (e: MouseEvent) => {
  375. const newX = e.clientX - rect.left - offsetX
  376. const newY = e.clientY - rect.top - offsetY
  377. const containerWidth = container.offsetWidth
  378. const containerHeight = container.offsetHeight
  379. block.x = Math.max(0, Math.min(newX, containerWidth - block.width))
  380. block.y = Math.max(0, Math.min(newY, containerHeight - block.height))
  381. block.ox = block.x - originX
  382. block.oy = originY - block.y
  383. }
  384. const upHandler = () => {
  385. document.removeEventListener('mousemove', moveHandler)
  386. document.removeEventListener('mouseup', upHandler)
  387. }
  388. document.addEventListener('mousemove', moveHandler)
  389. document.addEventListener('mouseup', upHandler)
  390. }
  391. const drag = (block: BlockItem) => {
  392. formState.region = [block.ox, block.oy, block.width, block.height]
  393. }
  394. const endDrag = (block: BlockItem) => {
  395. formState.region = [block.ox, block.oy, block.width, block.height]
  396. }
  397. // 获取容器边界
  398. const getContainerRect = () => {
  399. const container = document.querySelector('.blockArea') as HTMLElement
  400. return container?.getBoundingClientRect() || { left: 0, top: 0 }
  401. }
  402. const startResize = (block: BlockItem, e: MouseEvent) => {
  403. const startX = e.clientX
  404. const startY = e.clientY
  405. const initialWidth = block.width
  406. const initialHeight = block.height
  407. const moveHandler = (e: MouseEvent) => {
  408. const rect = getContainerRect()
  409. const deltaX = e.clientX - startX
  410. const deltaY = e.clientY - startY
  411. // 限制最小尺寸和容器边界
  412. block.width = Math.max(0, Math.min(initialWidth + deltaX, rect.width - block.x))
  413. block.height = Math.max(0, Math.min(initialHeight + deltaY, rect.height - block.y))
  414. }
  415. const upHandler = () => {
  416. document.removeEventListener('mousemove', moveHandler)
  417. document.removeEventListener('mouseup', upHandler)
  418. }
  419. document.addEventListener('mousemove', moveHandler)
  420. document.addEventListener('mouseup', upHandler)
  421. }
  422. const weekOptions = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
  423. const monthOptions = [
  424. 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,
  425. 28, 29, 30, 31,
  426. ]
  427. type FormState = {
  428. planName: string // 计划名称
  429. region: number[] // 检测区域 [left, top, width, height]
  430. eventType: number | null // 事件类型
  431. thresholdTime: number | null // 触发阈值
  432. mergeTime: number | null // 归并时间
  433. planTime: string[] // 计划时间
  434. effectType: 'week' | 'month' // 生效方式 周week、月month
  435. effectTimeRanges: (number | string)[] // 生效范围 周 1-7、月 1-31
  436. effectTimeFrame: string[] // 生效时段 单条 00:00:00 - 23:59:59
  437. effectTimeFrames: { startTime: string; endTime: string }[] // 生效时段 多条
  438. enable: boolean // 是否启用
  439. linkagePushWechatService: boolean // 是否开启服务号消息推送
  440. statisticsTime: string[] // 统计时间
  441. count?: number | null // 异常阈值
  442. timeThreshold?: number | null // 异常消失时间阈值(单位:秒)
  443. remark?: string // 备注
  444. }
  445. const formState = reactive<FormState>({
  446. planName: '',
  447. region: [0, 0, 50, 50],
  448. eventType: null,
  449. thresholdTime: 300,
  450. mergeTime: 30,
  451. planTime: [],
  452. effectType: 'week',
  453. effectTimeRanges: weekOptions,
  454. effectTimeFrame: [],
  455. effectTimeFrames: [],
  456. enable: true,
  457. linkagePushWechatService: true,
  458. statisticsTime: [],
  459. count: 3,
  460. timeThreshold: 300,
  461. })
  462. const initBlocks = () => {
  463. blocks.value = [
  464. {
  465. x: 0,
  466. y: 0,
  467. ox: formState.region[0],
  468. oy: formState.region[1],
  469. width: formState.region[2],
  470. height: formState.region[3],
  471. },
  472. ]
  473. }
  474. initBlocks()
  475. const thresholdTimeFormat = ref<'s' | 'min' | 'hour' | 'day'>('s') // 触发阈值 额外选择器
  476. const timeThresholdFormat = ref<'s' | 'min' | 'hour' | 'day'>('s') // 异常消失时间阈值 额外选择器
  477. const plainOptions = ref<(number | string)[]>(weekOptions)
  478. const checkState = reactive({
  479. indeterminate: true,
  480. checkAll: false,
  481. })
  482. const onCheckAllChange = (e: Event) => {
  483. const checked = (e.target as HTMLInputElement).checked
  484. checkState.checkAll = checked
  485. console.log('onCheckAllChange', e, checked)
  486. formState.effectTimeRanges = checked ? [...plainOptions.value] : []
  487. }
  488. // 星期映射数字
  489. const weekToNumMap: Record<string, number> = {
  490. 周一: 1,
  491. 周二: 2,
  492. 周三: 3,
  493. 周四: 4,
  494. 周五: 5,
  495. 周六: 6,
  496. 周日: 7,
  497. }
  498. // 数字映射星期
  499. const numToWeekMap: Record<number, string> = {
  500. 1: '周一',
  501. 2: '周二',
  502. 3: '周三',
  503. 4: '周四',
  504. 5: '周五',
  505. 6: '周六',
  506. 7: '周日',
  507. }
  508. watch(
  509. () => formState.effectTimeRanges,
  510. (val) => {
  511. checkState.indeterminate = !!val.length && val.length < plainOptions.value.length // 设置全选按钮的半选状态
  512. checkState.checkAll = val.length === plainOptions.value.length // 设置全选按钮的选中状态
  513. },
  514. {
  515. immediate: true,
  516. }
  517. )
  518. const safeParse = <T,>(str: string | undefined | null, fallback: T): T => {
  519. try {
  520. return JSON.parse(str ?? JSON.stringify(fallback)) as T
  521. } catch {
  522. return fallback
  523. }
  524. }
  525. interface ParamType {
  526. start_time?: string
  527. end_time?: string
  528. count?: number
  529. time_threshold?: number
  530. }
  531. interface TimeFrame {
  532. start_time?: string
  533. end_time?: string
  534. }
  535. interface AlarmTimePlan {
  536. startDate?: string
  537. stopDate?: string
  538. weekdays?: string
  539. monthDays?: string
  540. timeRange?: string
  541. }
  542. interface SourceData {
  543. name?: string
  544. remark?: string
  545. thresholdTime?: number
  546. mergeTime?: number
  547. eventVal?: number | null
  548. param?: string
  549. alarmTimePlan?: AlarmTimePlan
  550. alarmTimePlanTpl?: AlarmTimePlan
  551. region?: string
  552. enable?: number
  553. linkagePushWechatService?: number
  554. }
  555. // 是否为有效json字符串
  556. const isValidJSON = (str: string) => {
  557. try {
  558. JSON.parse(str)
  559. return true
  560. } catch {
  561. return false
  562. }
  563. }
  564. const echoFormState = (val: SourceData) => {
  565. const paramObj = safeParse<ParamType>(val.param, {})
  566. let weekdays: string[] = []
  567. let monthDays: string[] = []
  568. let timeFrames: TimeFrame[] = []
  569. let planTimes: string[] = []
  570. if (props.type === 'plan') {
  571. weekdays = safeParse<string[]>(val.alarmTimePlan?.weekdays, [])
  572. monthDays = safeParse<string[]>(val.alarmTimePlan?.monthDays, [])
  573. timeFrames = safeParse<TimeFrame[]>(val.alarmTimePlan?.timeRange, [])
  574. planTimes = [
  575. dayjs(val.alarmTimePlan?.startDate).format('YYYY-MM-DD') ?? '',
  576. dayjs(val.alarmTimePlan?.stopDate).format('YYYY-MM-DD') ?? '',
  577. ]
  578. }
  579. if (props.type === 'template') {
  580. weekdays = safeParse<string[]>(val.alarmTimePlanTpl?.weekdays, [])
  581. monthDays = safeParse<string[]>(val.alarmTimePlanTpl?.monthDays, [])
  582. timeFrames = safeParse<TimeFrame[]>(val.alarmTimePlanTpl?.timeRange, [])
  583. planTimes = [
  584. dayjs(val.alarmTimePlanTpl?.startDate).format('YYYY-MM-DD') ?? '',
  585. dayjs(val.alarmTimePlanTpl?.stopDate).format('YYYY-MM-DD') ?? '',
  586. ]
  587. }
  588. plainOptions.value = weekdays.length === 0 ? monthOptions : weekOptions
  589. let effectTimeRanges: (number | string)[] = []
  590. try {
  591. effectTimeRanges =
  592. weekdays.length === 0 ? monthDays : weekdays.map((item: string) => numToWeekMap[Number(item)])
  593. } catch {
  594. effectTimeRanges = []
  595. }
  596. return {
  597. planName: val.name ?? '',
  598. remark: val.remark ?? '',
  599. thresholdTime: typeof val.thresholdTime === 'number' ? val.thresholdTime : null,
  600. mergeTime: typeof val.mergeTime === 'number' ? val.mergeTime : null,
  601. eventType: val.eventVal ?? null,
  602. statisticsTime: [paramObj.start_time ?? null, paramObj.end_time ?? null] as string[],
  603. count: paramObj.count ?? null,
  604. timeThreshold: paramObj.time_threshold ?? null,
  605. planTime: planTimes,
  606. effectType: (weekdays.length === 0 ? 'month' : 'week') as 'week' | 'month',
  607. effectTimeRanges,
  608. effectTimeFrames: Array.isArray(timeFrames)
  609. ? timeFrames.map((item) => ({
  610. startTime: item?.start_time ?? '',
  611. endTime: item?.end_time ?? '',
  612. }))
  613. : [],
  614. region: isValidJSON(val?.region ?? '') && JSON.parse(val?.region ?? '[0, 0, 50, 50]'),
  615. enable: val?.enable === 1,
  616. linkagePushWechatService: val?.linkagePushWechatService === 1,
  617. }
  618. }
  619. watch(
  620. () => props.open,
  621. (value) => {
  622. const val = props.data as SourceData
  623. console.log('🌸🌸 监听data用于编辑回显数据 🌸🌸', value, val)
  624. if (value && val) {
  625. formState.planName = echoFormState(val).planName
  626. formState.remark = echoFormState(val).remark
  627. formState.thresholdTime = echoFormState(val).thresholdTime
  628. formState.mergeTime = echoFormState(val).mergeTime
  629. formState.eventType = echoFormState(val).eventType
  630. formState.statisticsTime = echoFormState(val).statisticsTime
  631. formState.count = echoFormState(val).count
  632. formState.timeThreshold = echoFormState(val).timeThreshold
  633. formState.planTime = echoFormState(val).planTime
  634. formState.effectType = echoFormState(val).effectType
  635. formState.effectTimeRanges = echoFormState(val).effectTimeRanges
  636. formState.effectTimeFrames = echoFormState(val).effectTimeFrames
  637. formState.region = echoFormState(val).region
  638. formState.enable = echoFormState(val).enable
  639. formState.linkagePushWechatService = echoFormState(val).linkagePushWechatService
  640. initBlocks()
  641. }
  642. },
  643. { immediate: true }
  644. )
  645. // 生效方式变化 周week、月month
  646. const effectTypeChange = (e: Event) => {
  647. const value = (e.target as HTMLInputElement).value
  648. console.log('effectTypeChange', e, value)
  649. if (value === 'week') {
  650. plainOptions.value = weekOptions
  651. formState.effectTimeRanges = weekOptions
  652. } else if (value === 'month') {
  653. plainOptions.value = monthOptions
  654. formState.effectTimeRanges = monthOptions
  655. }
  656. /* 置全选按钮的状态 */
  657. checkState.indeterminate =
  658. !!formState.effectTimeRanges.length &&
  659. formState.effectTimeRanges.length < plainOptions.value.length // 设置全选按钮的半选状态
  660. checkState.checkAll = formState.effectTimeRanges.length === plainOptions.value.length // 设置全选按钮的选中状态
  661. }
  662. // 添加时间段
  663. const addEffectTime = () => {
  664. console.log('addEffectTime', formState.effectTimeFrame)
  665. if (!formState.effectTimeFrame || !formState.effectTimeFrame.length) {
  666. message.warn('请选择时间段')
  667. return
  668. }
  669. formState.effectTimeFrames.push({
  670. startTime: formState.effectTimeFrame[0],
  671. endTime: formState.effectTimeFrame[1],
  672. })
  673. // formState.effectTimeFrame = [] // 清空选择的时间段
  674. }
  675. // 删除已添加的时间段
  676. const deleteEffectTimeItem = (e: Event, index: number) => {
  677. console.log('deleteEffectTimeItem', e, index)
  678. formState.effectTimeFrames.splice(index, 1)
  679. }
  680. // 关闭弹窗
  681. const cancel = () => {
  682. formRef?.value?.resetFields()
  683. emit('update:open', false)
  684. // 重置表单
  685. formState.planName = ''
  686. formState.region = [0, 0, 50, 50]
  687. formState.eventType = null
  688. formState.thresholdTime = 300
  689. formState.mergeTime = 30
  690. formState.planTime = []
  691. formState.effectType = 'week'
  692. plainOptions.value = weekOptions
  693. formState.effectTimeRanges = weekOptions
  694. formState.effectTimeFrame = []
  695. formState.effectTimeFrames = []
  696. formState.enable = true
  697. formState.linkagePushWechatService = true
  698. formState.statisticsTime = []
  699. formState.count = 3
  700. formState.timeThreshold = 300
  701. formState.remark = ''
  702. }
  703. const eventTypeList = ref<{ label: string; value: string }[]>([])
  704. // 获取事件类型下拉列表
  705. const fetchEventTypeList = async () => {
  706. try {
  707. const res = await alarmApi.getAlarmEventTypeList()
  708. console.log('获取事件类型下拉列表成功✅', res)
  709. const data = res.data
  710. eventTypeList.value =
  711. (Array.isArray(data) &&
  712. data.map((item) => ({
  713. label: item.eventDesc,
  714. value: item.eventVal,
  715. }))) ||
  716. []
  717. } catch (err) {
  718. console.log('获取事件类型下拉列表失败❌', err)
  719. }
  720. }
  721. fetchEventTypeList()
  722. function thresholdTimeFormatValue() {
  723. if (thresholdTimeFormat.value === 's') {
  724. return Number(formState.thresholdTime) // 触发阈值
  725. } else if (thresholdTimeFormat.value === 'min') {
  726. return Number(formState.thresholdTime) * 60 // 触发阈值
  727. } else if (thresholdTimeFormat.value === 'hour') {
  728. return Number(formState.thresholdTime) * 60 * 60 // 触发阈值
  729. } else if (thresholdTimeFormat.value === 'day') {
  730. return Number(formState.thresholdTime) * 24 * 60 * 60 // 触发阈值
  731. }
  732. }
  733. function timeThresholdFormatValue() {
  734. if (timeThresholdFormat.value === 's') {
  735. return Number(formState.timeThreshold) // 触发阈值
  736. } else if (timeThresholdFormat.value === 'min') {
  737. return Number(formState.timeThreshold) * 60 // 触发阈值
  738. } else if (timeThresholdFormat.value === 'hour') {
  739. return Number(formState.timeThreshold) * 60 * 60 // 触发阈值
  740. } else if (timeThresholdFormat.value === 'day') {
  741. return Number(formState.timeThreshold) * 24 * 60 * 60 // 触发阈值
  742. }
  743. }
  744. const submitLoading = ref(false)
  745. // 确定
  746. const submit = () => {
  747. formRef?.value
  748. ?.validate()
  749. .then(() => {
  750. console.log('校验通过', formState)
  751. let paramData = {}
  752. if ([1, 2, 3].includes(formState.eventType as number)) {
  753. paramData = {}
  754. console.log('🔥paramData🔥', paramData)
  755. } else if ([4, 5, 8].includes(formState.eventType as number)) {
  756. paramData = {
  757. start_time: formState.statisticsTime[0],
  758. end_time: formState.statisticsTime[1],
  759. }
  760. console.log('🔥paramData🔥', paramData)
  761. } else if ([6, 7].includes(formState.eventType as number)) {
  762. paramData = {
  763. start_time: formState.statisticsTime[0],
  764. end_time: formState.statisticsTime[1],
  765. count: isNaN(Number(formState.count)) ? 0 : Number(formState.count),
  766. }
  767. console.log('🔥paramData🔥', paramData)
  768. } else if ([9].includes(formState.eventType as number)) {
  769. paramData = {
  770. time_threshold: isNaN(Number(formState.timeThreshold)) ? 0 : timeThresholdFormatValue(),
  771. }
  772. console.log('🔥paramData🔥', paramData)
  773. }
  774. const params = {
  775. alarmPlanId: props.alarmPlanId || undefined, // 告警计划ID 编辑时传入
  776. clientId: props.clientId, // 设备ID
  777. name: formState.planName, // 计划名称
  778. remark: formState.remark || '', // 备注
  779. thresholdTime: thresholdTimeFormatValue(), // 触发阈值
  780. mergeTime: Number(formState.mergeTime) || 30, // 归并时间
  781. eventVal: formState.eventType as number, // 事件类型 与 param 有联动关系
  782. param: JSON.stringify(paramData), // 事件参数 与 eventVal 有联动关系
  783. region: [1, 2, 3, 9].includes(formState.eventType as number)
  784. ? JSON.stringify(formState.region)
  785. : '[]', // 检测区域
  786. linkagePushWechatService: Number(formState.linkagePushWechatService) as 0 | 1, // 是否开启服务号消息推送 1:开启 0:关闭
  787. enable: Number(formState.enable) as 0 | 1, // 是否启用 0否 1是
  788. // 生效方式
  789. alarmTimePlan: {
  790. startDate: formState.planTime[0], // 计划开始时间
  791. stopDate: formState.planTime[1], // 计划结束时间
  792. // 生效时段
  793. timeRange: JSON.stringify(
  794. formState.effectTimeFrames.map((item) => ({
  795. start_time: item.startTime,
  796. end_time: item.endTime,
  797. }))
  798. ),
  799. monthDays: JSON.stringify(
  800. formState.effectType === 'month' ? formState.effectTimeRanges : []
  801. ),
  802. weekdays: JSON.stringify(
  803. formState.effectType === 'week'
  804. ? formState.effectTimeRanges.map((item) => weekToNumMap[item])
  805. : []
  806. ),
  807. },
  808. }
  809. console.log('🚀🚀🚀提交参数', params)
  810. if (formState.effectTimeFrames.length === 0) {
  811. message.warn('请添加生效时段')
  812. return
  813. }
  814. if (formState.effectTimeRanges.length === 0) {
  815. message.warn('请选择生效方式的范围')
  816. return
  817. }
  818. // if ([1, 2, 3, 9].includes(formState.eventType as number) && formState.region.length !== 4) {
  819. // message.warn('请选择检测区域')
  820. // return
  821. // }
  822. submitLoading.value = true
  823. // alarmApi
  824. // .saveAlarmPlan(params)
  825. // .then((res) => {
  826. // console.log('添加成功', res)
  827. // submitLoading.value = false
  828. // message.success('添加成功')
  829. // emit('success')
  830. // cancel()
  831. // })
  832. // .catch(() => {
  833. // submitLoading.value = false
  834. // })
  835. if (props.type === 'plan') savePlan(params)
  836. if (props.type === 'template') saveTemplate(params)
  837. })
  838. .catch((err) => {
  839. console.log('校验失败', err)
  840. })
  841. }
  842. // 保存计划
  843. const savePlan = async (params: AlarmPlanParams) => {
  844. console.log('保存计划')
  845. try {
  846. await alarmApi
  847. .saveAlarmPlan(params)
  848. .then((res) => {
  849. console.log('添加成功', res)
  850. submitLoading.value = false
  851. message.success('添加成功')
  852. emit('success')
  853. cancel()
  854. })
  855. .catch(() => {
  856. submitLoading.value = false
  857. })
  858. } catch (error) {
  859. console.log('添加失败', error)
  860. message.error('添加失败')
  861. }
  862. }
  863. // 保存模板
  864. const saveTemplate = async (params: AlarmPlanParams) => {
  865. console.log('saveTemplate', formState)
  866. try {
  867. await alarmApi
  868. .saveAlarmPlanTemplate({
  869. ...params,
  870. id: props.alarmPlanId as number,
  871. name: params.name,
  872. eventVal: params.eventVal,
  873. param: params.param,
  874. region: params.region,
  875. linkagePushWechatService: params.linkagePushWechatService,
  876. enable: params.enable,
  877. alarmTimePlanTpl: params.alarmTimePlan,
  878. })
  879. .then((res) => {
  880. console.log('添加成功', res)
  881. submitLoading.value = false
  882. message.success('添加成功')
  883. emit('success')
  884. cancel()
  885. })
  886. .catch(() => {
  887. submitLoading.value = false
  888. })
  889. } catch (error) {
  890. console.log('添加失败', error)
  891. message.error('添加失败')
  892. }
  893. }
  894. </script>
  895. <style scoped lang="less">
  896. :deep(.ant-modal) {
  897. .footer {
  898. text-align: right;
  899. }
  900. .ant-modal-body {
  901. padding: 12px 0;
  902. }
  903. }
  904. :deep(.ant-checkbox-group) {
  905. .ant-checkbox + span {
  906. min-width: 32px;
  907. }
  908. }
  909. :deep(.ant-tag) {
  910. margin-inline-end: 0 !important;
  911. }
  912. .eventTypeBox {
  913. margin-top: 12px;
  914. color: #555;
  915. font-size: 14px;
  916. padding: 20px 12px;
  917. background: #fafafa;
  918. border-radius: 8px;
  919. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
  920. &-item {
  921. margin-bottom: 12px;
  922. display: flex;
  923. align-items: center;
  924. gap: 8px;
  925. &-label {
  926. width: 100px;
  927. }
  928. &-labelend {
  929. width: 140px;
  930. }
  931. &:last-child {
  932. margin-bottom: 0;
  933. }
  934. :deep(.ant-form-item) {
  935. flex: 1;
  936. margin-bottom: 0;
  937. }
  938. }
  939. }
  940. .viewer {
  941. padding: 10px;
  942. min-width: 500px;
  943. flex-shrink: 0;
  944. &-header {
  945. display: flex;
  946. justify-content: space-between;
  947. padding-bottom: 20px;
  948. &-title {
  949. font-size: 16px;
  950. font-weight: 600;
  951. line-height: 24px;
  952. }
  953. &-subtitle {
  954. font-size: 14px;
  955. color: #666;
  956. }
  957. }
  958. &-content {
  959. display: flex;
  960. gap: 20px;
  961. }
  962. }
  963. .mapBox {
  964. background-color: #e0e0e0;
  965. background-image:
  966. linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
  967. linear-gradient(to right, rgba(0, 0, 0, 0.1) 1px, transparent 1px);
  968. background-size: 20px 20px;
  969. position: relative;
  970. flex-shrink: 0;
  971. // 添加黑边框
  972. &::before {
  973. content: '';
  974. position: absolute;
  975. top: -5px;
  976. left: -5px;
  977. width: calc(100% + 10px);
  978. height: calc(100% + 10px);
  979. border: 5px solid rgba(0, 0, 0, 0.8);
  980. box-sizing: border-box;
  981. pointer-events: none;
  982. }
  983. }
  984. .block-item {
  985. background: rgba(24, 144, 255, 0.1);
  986. .resize-handle {
  987. position: absolute;
  988. right: -4px;
  989. bottom: -4px;
  990. width: 15px;
  991. height: 15px;
  992. background: #1890ff;
  993. cursor: nwse-resize;
  994. font-size: 12px;
  995. color: #fff;
  996. display: flex;
  997. align-items: center;
  998. justify-content: center;
  999. }
  1000. }
  1001. </style>