Преглед на файлове

新增算法 选择报警类型

王哲 преди 2 седмици
родител
ревизия
8ac85cea13
променени са 1 файла, в които са добавени 354 реда и са изтрити 0 реда
  1. 354 0
      monitor_ui/src/views/business/ai/algorithm/CustomSelect.vue

+ 354 - 0
monitor_ui/src/views/business/ai/algorithm/CustomSelect.vue

@@ -0,0 +1,354 @@
+<template>
+  <div class="smart-select" ref="selectWrapper">
+    <!-- 合并的输入框 -->
+    <input
+        ref="inputRef"
+        v-model="inputValue"
+        :placeholder="placeholder"
+        class="smart-input"
+        :class="{ 'is-focused': showOptions }"
+        @focus="handleFocus"
+        @blur="handleBlur"
+        @input="handleInput"
+        @keydown="handleKeydown"
+    />
+
+    <!-- 下拉选项 -->
+    <div v-if="showOptions && filteredOptions.length > 0" class="smart-dropdown">
+      <div class="options-list">
+        <div
+            v-for="(option, index) in filteredOptions"
+            :key="getOptionValue(option)"
+            @mousedown="handleOptionClick(option)"
+            class="option-item"
+            :class="{
+            'is-selected': isSelected(option),
+            'is-hovered': hoverIndex === index
+          }"
+        >
+          {{ getOptionLabel(option) }}
+        </div>
+      </div>
+    </div>
+
+    <!-- 无匹配数据提示 -->
+    <div v-if="showOptions && filteredOptions.length === 0" class="no-match">
+      无匹配数据
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
+
+  const props = defineProps({
+    modelValue: {
+      type: [String, Number],
+      default: ''
+    },
+    options: {
+      type: Array,
+      default: () => []
+    },
+    placeholder: {
+      type: String,
+      default: '请选择或输入'
+    },
+    valueKey: {
+      type: String,
+      default: 'alarmType'
+    },
+    labelKey: {
+      type: String,
+      default: 'alarmTypeName'
+    },
+    // 是否允许输入自定义值
+    allowCustom: {
+      type: Boolean,
+      default: false
+    },
+    // 防抖延迟(毫秒)
+    debounce: {
+      type: Number,
+      default: 300
+    }
+  })
+
+  const emit = defineEmits(['update:modelValue', 'change', 'search'])
+
+  // 响应式数据
+  const showOptions = ref(false)
+  const inputValue = ref('')
+  const hoverIndex = ref(-1)
+  const inputRef = ref(null)
+  const selectWrapper = ref(null)
+  const debounceTimer = ref(null)
+
+  // 获取选项值
+  const getOptionValue = (option) => {
+    return typeof option === 'object' ? option[props.valueKey] : option
+  }
+
+  // 获取选项标签
+  const getOptionLabel = (option) => {
+    if (typeof option === 'object') {
+      return option[props.labelKey] || option[props.valueKey]
+    }
+    return option
+  }
+
+  // 查找当前值对应的选项
+  const findCurrentOption = (value) => {
+    return props.options.find(option => getOptionValue(option) === value)
+  }
+
+  // 过滤选项
+  const filteredOptions = computed(() => {
+    if (!inputValue.value) {
+      return props.options
+    }
+
+    const searchText = inputValue.value.toLowerCase()
+    return props.options.filter(option => {
+      const label = getOptionLabel(option).toLowerCase()
+      return label.includes(searchText)
+    })
+  })
+
+  // 检查是否选中
+  const isSelected = (option) => {
+    return getOptionValue(option) === props.modelValue
+  }
+
+  // 处理焦点事件
+  const handleFocus = () => {
+    showOptions.value = true
+    hoverIndex.value = -1
+  }
+
+  // 处理失去焦点事件
+  const handleBlur = () => {
+    setTimeout(() => {
+      showOptions.value = false
+      hoverIndex.value = -1
+
+      // 如果允许自定义值,且当前输入值不在选项中,则使用输入值
+      if (props.allowCustom && inputValue.value && !findCurrentOption(props.modelValue)) {
+        emit('update:modelValue', inputValue.value)
+        emit('change', inputValue.value)
+      }
+    }, 200)
+  }
+
+  // 处理输入事件(带防抖)
+  const handleInput = () => {
+    showOptions.value = true
+    hoverIndex.value = -1
+
+    // 清除之前的定时器
+    if (debounceTimer.value) {
+      clearTimeout(debounceTimer.value)
+    }
+
+    // 设置防抖
+    debounceTimer.value = setTimeout(() => {
+      emit('search', inputValue.value)
+    }, props.debounce)
+  }
+
+  // 处理选项点击
+  const handleOptionClick = (option) => {
+    const value = getOptionValue(option)
+    const label = getOptionLabel(option)
+
+    inputValue.value = label
+    emit('update:modelValue', value)
+    emit('change', value)
+    showOptions.value = false
+    hoverIndex.value = -1
+  }
+
+  // 键盘导航
+  const handleKeydown = (event) => {
+    switch (event.key) {
+      case 'ArrowDown':
+        event.preventDefault()
+        if (showOptions.value && filteredOptions.value.length > 0) {
+          hoverIndex.value = (hoverIndex.value + 1) % filteredOptions.value.length
+          scrollToHoveredOption()
+        } else {
+          showOptions.value = true
+        }
+        break
+
+      case 'ArrowUp':
+        event.preventDefault()
+        if (showOptions.value && filteredOptions.value.length > 0) {
+          hoverIndex.value = hoverIndex.value <= 0
+            ? filteredOptions.value.length - 1
+            : hoverIndex.value - 1
+          scrollToHoveredOption()
+        }
+        break
+
+      case 'Enter':
+        event.preventDefault()
+        if (showOptions.value && hoverIndex.value >= 0 && filteredOptions.value[hoverIndex.value]) {
+          // 选择当前悬停的选项
+          handleOptionClick(filteredOptions.value[hoverIndex.value])
+        } else if (props.allowCustom && inputValue.value) {
+          // 使用自定义输入值
+          emit('update:modelValue', inputValue.value)
+          emit('change', inputValue.value)
+          showOptions.value = false
+        }
+        break
+
+      case 'Escape':
+        showOptions.value = false
+        hoverIndex.value = -1
+        break
+
+      case 'Tab':
+        showOptions.value = false
+        hoverIndex.value = -1
+        break
+    }
+  }
+
+  // 滚动到悬停的选项
+  const scrollToHoveredOption = () => {
+    nextTick(() => {
+      const optionElements = selectWrapper.value?.querySelectorAll('.option-item')
+      if (optionElements && optionElements[hoverIndex.value]) {
+        optionElements[hoverIndex.value].scrollIntoView({
+          block: 'nearest',
+          behavior: 'smooth'
+        })
+      }
+    })
+  }
+
+  // 点击外部关闭
+  const handleClickOutside = (event) => {
+    if (selectWrapper.value && !selectWrapper.value.contains(event.target)) {
+      showOptions.value = false
+      hoverIndex.value = -1
+    }
+  }
+
+  // 监听外部值变化
+  watch(() => props.modelValue, (newValue) => {
+    const option = findCurrentOption(newValue)
+    if (option) {
+      inputValue.value = getOptionLabel(option)
+    } else if (props.allowCustom) {
+      inputValue.value = newValue
+    } else {
+      inputValue.value = ''
+    }
+  }, { immediate: true })
+
+  // 监听选项列表变化
+  watch(() => props.options, () => {
+    // 当选项列表更新时,重新设置当前值的显示
+    const option = findCurrentOption(props.modelValue)
+    if (option) {
+      inputValue.value = getOptionLabel(option)
+    }
+  })
+
+  // 生命周期
+  onMounted(() => {
+    document.addEventListener('click', handleClickOutside)
+  })
+
+  onUnmounted(() => {
+    document.removeEventListener('click', handleClickOutside)
+    if (debounceTimer.value) {
+      clearTimeout(debounceTimer.value)
+    }
+  })
+</script>
+
+<style scoped>
+  .smart-select {
+    position: relative;
+    width: 370px;
+  }
+
+  .smart-input {
+    width: 100%;
+    padding: 8px 12px;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    outline: none;
+    font-size: 14px;
+    color: #606266;
+    transition: border-color 0.2s;
+  }
+
+  .smart-input:focus {
+    border-color: #409eff;
+  }
+
+  .smart-input.is-focused {
+    border-color: #409eff;
+  }
+
+  .smart-dropdown {
+    position: absolute;
+    top: 100%;
+    left: 0;
+    right: 0;
+    background: white;
+    border: 1px solid #e4e7ed;
+    border-radius: 4px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+    z-index: 1000;
+    margin-top: 4px;
+    max-height: 200px;
+    overflow: hidden;
+  }
+
+  .options-list {
+    max-height: 200px;
+    overflow-y: auto;
+  }
+
+  .option-item {
+    padding: 8px 12px;
+    cursor: pointer;
+    color: #606266;
+    transition: all 0.2s;
+    font-size: 14px;
+  }
+
+  .option-item:hover,
+  .option-item.is-hovered {
+    background: #f5f7fa;
+  }
+
+  .option-item.is-selected {
+    color: #409eff;
+    background: #f0f9ff;
+    font-weight: 500;
+  }
+
+  .no-match {
+    position: absolute;
+    top: 100%;
+    left: 0;
+    right: 0;
+    padding: 16px;
+    text-align: center;
+    color: #909399;
+    font-size: 14px;
+    background: white;
+    border: 1px solid #e4e7ed;
+    border-radius: 4px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+    z-index: 1000;
+    margin-top: 4px;
+  }
+</style>