فهرست منبع

perf:【IoT 物联网】场景联动场景描述输入组件优化使用 teleport 传送到 body,不受父容器 overflow 影响

puhui999 10 ماه پیش
والد
کامیت
7723358422
1فایلهای تغییر یافته به همراه108 افزوده شده و 35 حذف شده
  1. 108 35
      src/views/iot/rule/scene/components/inputs/DescriptionInput.vue

+ 108 - 35
src/views/iot/rule/scene/components/inputs/DescriptionInput.vue

@@ -2,6 +2,7 @@
 <template>
   <div class="description-input">
     <el-input
+      ref="inputRef"
       v-model="localValue"
       type="textarea"
       placeholder="请输入场景描述(可选)"
@@ -11,38 +12,45 @@
       resize="none"
       @input="handleInput"
     />
-    
+
     <!-- 描述模板 -->
-    <div v-if="showTemplates" class="templates">
-      <div class="templates-header">
-        <span class="templates-title">描述模板</span>
-        <el-button 
-          type="text" 
-          size="small" 
-          @click="showTemplates = false"
-        >
-          <Icon icon="ep:close" />
-        </el-button>
-      </div>
-      <div class="templates-list">
-        <div
-          v-for="template in descriptionTemplates"
-          :key="template.title"
-          class="template-item"
-          @click="applyTemplate(template)"
-        >
-          <div class="template-title">{{ template.title }}</div>
-          <div class="template-content">{{ template.content }}</div>
+    <teleport to="body">
+      <div
+        v-if="showTemplates"
+        ref="templateDropdownRef"
+        class="templates"
+        :style="dropdownStyle"
+      >
+        <div class="templates-header">
+          <span class="templates-title">描述模板</span>
+          <el-button
+            type="text"
+            size="small"
+            @click="showTemplates = false"
+          >
+            <Icon icon="ep:close" />
+          </el-button>
+        </div>
+        <div class="templates-list">
+          <div
+            v-for="template in descriptionTemplates"
+            :key="template.title"
+            class="template-item"
+            @click="applyTemplate(template)"
+          >
+            <div class="template-title">{{ template.title }}</div>
+            <div class="template-content">{{ template.content }}</div>
+          </div>
         </div>
       </div>
-    </div>
-    
+    </teleport>
+
     <!-- 模板按钮 -->
     <div v-if="!localValue && !showTemplates" class="template-trigger">
-      <el-button 
-        type="text" 
-        size="small" 
-        @click="showTemplates = true"
+      <el-button
+        type="text"
+        size="small"
+        @click="toggleTemplates"
       >
         <Icon icon="ep:document" class="mr-1" />
         使用模板
@@ -73,6 +81,9 @@ const localValue = useVModel(props, 'modelValue', emit, {
 })
 
 const showTemplates = ref(false)
+const templateDropdownRef = ref()
+const inputRef = ref()
+const dropdownStyle = ref({})
 
 // 描述模板
 const descriptionTemplates = [
@@ -98,6 +109,38 @@ const descriptionTemplates = [
   }
 ]
 
+// 计算下拉框位置
+const calculateDropdownPosition = () => {
+  if (!inputRef.value) return
+
+  const inputElement = inputRef.value.$el || inputRef.value
+  const rect = inputElement.getBoundingClientRect()
+  const viewportHeight = window.innerHeight
+  const dropdownHeight = 300 // 预估下拉框高度
+
+  let top = rect.bottom + 4
+  let left = rect.left
+
+  // 如果下方空间不够,显示在上方
+  if (top + dropdownHeight > viewportHeight) {
+    top = rect.top - dropdownHeight - 4
+  }
+
+  // 确保不超出左右边界
+  const maxLeft = window.innerWidth - 400 // 下拉框最大宽度
+  if (left > maxLeft) {
+    left = maxLeft
+  }
+  if (left < 10) {
+    left = 10
+  }
+
+  dropdownStyle.value = {
+    top: `${top}px`,
+    left: `${left}px`
+  }
+}
+
 const handleInput = (value: string) => {
   if (value.length > 0) {
     showTemplates.value = false
@@ -108,6 +151,36 @@ const applyTemplate = (template: any) => {
   localValue.value = template.content
   showTemplates.value = false
 }
+
+const toggleTemplates = () => {
+  showTemplates.value = !showTemplates.value
+  if (showTemplates.value) {
+    nextTick(() => {
+      calculateDropdownPosition()
+    })
+  }
+}
+
+// 点击外部关闭下拉框
+const handleClickOutside = (event: Event) => {
+  if (templateDropdownRef.value && !templateDropdownRef.value.contains(event.target as Node) &&
+      inputRef.value && !inputRef.value.$el.contains(event.target as Node)) {
+    showTemplates.value = false
+  }
+}
+
+// 监听窗口大小变化和点击事件
+onMounted(() => {
+  window.addEventListener('resize', calculateDropdownPosition)
+  window.addEventListener('scroll', calculateDropdownPosition)
+  document.addEventListener('click', handleClickOutside)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', calculateDropdownPosition)
+  window.removeEventListener('scroll', calculateDropdownPosition)
+  document.removeEventListener('click', handleClickOutside)
+})
 </script>
 
 <style scoped>
@@ -117,16 +190,16 @@ const applyTemplate = (template: any) => {
 }
 
 .templates {
-  position: absolute;
-  top: 100%;
-  left: 0;
-  right: 0;
-  z-index: 1000;
+  position: fixed;
+  z-index: 9999;
   background: white;
   border: 1px solid var(--el-border-color-light);
-  border-radius: 4px;
-  box-shadow: var(--el-box-shadow-light);
-  margin-top: 4px;
+  border-radius: 6px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  min-width: 300px;
+  max-width: 400px;
+  max-height: 400px;
+  overflow: hidden;
 }
 
 .templates-header {