123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741 |
- <template>
- <div ref="mod">
- <a-modal
- :get-container="() => $refs.mod"
- :open="props.open"
- :title="modelTitle"
- :mask-closable="false"
- width="600px"
- @cancel="cancel"
- :footer="null"
- >
- <a-form
- ref="formRef"
- :model="formState"
- :label-col="{ style: { width: '80px' } }"
- hideRequiredMark
- >
- <a-form-item
- label="计划名称"
- name="planName"
- :rules="[{ required: true, message: '请输入计划名称' }]"
- >
- <a-input
- v-model:value.trim="formState.planName"
- placeholder="请输入计划名称"
- :maxlength="20"
- show-count
- allow-clear
- />
- </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"
- :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
- label="计划时间"
- name="planTime"
- :rules="[{ type: 'array' as const, required: true, message: '请选择计划时间' }]"
- >
- <a-range-picker
- v-model:value="formState.planTime"
- style="width: 100%"
- :show-time="false"
- valueFormat="YYYY-MM-DD"
- />
- </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"
- 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 && !formState.effectTimeFrames.length"
- style="color: #aaa; font-size: 14px"
- >⚠️暂无生效时段</span
- >
- <a-space wrap v-else>
- <a-tag
- v-for="(item, index) in formState.effectTimeFrames"
- :key="index"
- closable
- style="font-size: 14px; padding: 4px 10px"
- @close="deleteEffectTimeItem($event, index)"
- >{{ item.startTime }} - {{ item.endTime }}</a-tag
- >
- </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 label="检测区域" name="region">
- 检测区域选择组件 {{ formState.region }}
- </a-form-item>
- <a-form-item label="是否启用">
- <a-switch v-model:checked="formState.enable" />
- </a-form-item>
- </a-form>
- <div class="footer">
- <a-space>
- <a-button @click="cancel">取消</a-button>
- <a-button type="primary" :loading="submitLoading" @click="submit">保存</a-button>
- </a-space>
- </div>
- </a-modal>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, reactive, watch, computed } from 'vue'
- import { message, type FormInstance } from 'ant-design-vue'
- import * as alarmApi from '@/api/alarm'
- defineOptions({
- name: 'AlarmPlanModal',
- })
- 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 // 编辑数据
- }
- const emit = defineEmits<{
- (e: 'update:open', value: boolean): void
- (e: 'success', value: void): void
- }>()
- const props = withDefaults(defineProps<Props>(), {
- open: false,
- title: '告警计划',
- clientId: '',
- alarmPlanId: null,
- data: undefined,
- })
- const modelTitle = computed(() => {
- return props.alarmPlanId ? '编辑告警计划' : '新增告警计划'
- })
- const weekOptions = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
- const monthOptions = [
- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
- 28, 29, 30, 31,
- ]
- type FormState = {
- planName: string // 计划名称
- region: number[] // 检测区域 [left, top, width, height]
- eventType: number | null // 事件类型
- thresholdTime: number | null // 触发阈值
- mergeTime: number | null // 归并时间
- planTime: string[] // 计划时间
- effectType: 'week' | 'month' // 生效方式 周week、月month
- effectTimeRanges: (number | string)[] // 生效范围 周 1-7、月 1-31
- effectTimeFrame: string[] // 生效时段 单条 00:00:00 - 23:59:59
- effectTimeFrames: { startTime: string; endTime: string }[] // 生效时段 多条
- enable: boolean // 是否启用
- statisticsTime: string[] // 统计时间
- count?: number | null // 异常阈值
- timeThreshold?: number | null // 异常消失时间阈值(单位:秒)
- remark?: string // 备注
- }
- const formState = reactive<FormState>({
- planName: '',
- region: [-200, 200, 400, 400],
- eventType: null,
- thresholdTime: 300,
- mergeTime: 30,
- planTime: [],
- effectType: 'week',
- effectTimeRanges: weekOptions,
- effectTimeFrame: [],
- effectTimeFrames: [],
- enable: true,
- statisticsTime: [],
- count: 3,
- timeThreshold: 300,
- })
- 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,
- })
- const onCheckAllChange = (e: Event) => {
- const checked = (e.target as HTMLInputElement).checked
- checkState.checkAll = checked
- console.log('onCheckAllChange', e, checked)
- 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.checkAll = val.length === plainOptions.value.length // 设置全选按钮的选中状态
- },
- {
- immediate: true,
- }
- )
- 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 ?? '[]'),
- 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
- }
- },
- { immediate: true }
- )
- // 生效方式变化 周week、月month
- const effectTypeChange = (e: Event) => {
- const value = (e.target as HTMLInputElement).value
- console.log('effectTypeChange', e, value)
- if (value === 'week') {
- plainOptions.value = weekOptions
- formState.effectTimeRanges = weekOptions
- } else if (value === 'month') {
- plainOptions.value = monthOptions
- formState.effectTimeRanges = monthOptions
- }
- /* 置全选按钮的状态 */
- checkState.indeterminate =
- !!formState.effectTimeRanges.length &&
- formState.effectTimeRanges.length < plainOptions.value.length // 设置全选按钮的半选状态
- checkState.checkAll = formState.effectTimeRanges.length === plainOptions.value.length // 设置全选按钮的选中状态
- }
- // 添加时间段
- const addEffectTime = () => {
- console.log('addEffectTime', formState.effectTimeFrame)
- if (!formState.effectTimeFrame || !formState.effectTimeFrame.length) {
- message.warn('请选择时间段')
- return
- }
- formState.effectTimeFrames.push({
- startTime: formState.effectTimeFrame[0],
- endTime: formState.effectTimeFrame[1],
- })
- // formState.effectTimeFrame = [] // 清空选择的时间段
- }
- // 删除已添加的时间段
- const deleteEffectTimeItem = (e: Event, index: number) => {
- console.log('deleteEffectTimeItem', e, index)
- formState.effectTimeFrames.splice(index, 1)
- }
- // 关闭弹窗
- const cancel = () => {
- formRef?.value?.resetFields()
- emit('update:open', false)
- // 重置表单
- formState.planName = ''
- formState.region = []
- 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)
- // 确定
- const submit = () => {
- formRef?.value
- ?.validate()
- .then(() => {
- console.log('校验通过', formState)
- 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: 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 (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)
- })
- }
- </script>
- <style scoped lang="less">
- :deep(.ant-modal) {
- .footer {
- text-align: right;
- }
- .ant-modal-body {
- padding: 12px 0;
- }
- }
- :deep(.ant-checkbox-group) {
- .ant-checkbox + span {
- min-width: 32px;
- }
- }
- :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;
- }
- }
- }
- </style>
|