Просмотр исходного кода

路口控制卡片新增锁定时间下拉框,优化 DropdownSelect 组件

  1. IntersectionControlCard 新增锁定时间下拉框(30s~300s,步长30s),使用 DropdownSelect 组件
  2. DropdownSelect 组件重构:新增 appendToBody 模式解决 overflow:hidden 容器遮挡问题,新增 neon
  霓虹主题,下拉菜单支持滚动(max-height)和上下翻转定位
画安 недель назад: 2
Родитель
Сommit
00aafa272f

+ 223 - 137
src/components/ui/DropdownSelect.vue

@@ -1,26 +1,18 @@
 <template>
-  <div class="custom-dropdown" ref="dropdown" :class="[`theme-${theme}`, { 'size-auto': size === 'auto' }]">
-    <div 
-      class="dropdown-trigger" 
-      :class="{ 'is-open': isOpen }" 
-      @click="toggleDropdown"
-    >
+  <div class="custom-dropdown" ref="dropdown" :class="[`theme-${theme}`, { 'size-auto': size === 'auto' }]"
+    @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
+    <div class="dropdown-trigger" :class="{ 'is-open': isOpen }" @click="toggleDropdown">
       <span class="trigger-text">{{ currentLabel }}</span>
       <i class="arrow-icon"></i>
     </div>
 
     <transition name="fade">
-      <div class="dropdown-menu" v-show="isOpen">
+      <div class="dropdown-menu" :class="`theme-${theme}`" v-show="isOpen" ref="menu" :style="menuStyle"
+        @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
         <div class="menu-arrow"></div>
-        
         <div class="menu-list">
-          <div 
-            class="menu-item" 
-            v-for="item in options" 
-            :key="item.value"
-            :class="{ 'is-active': value === item.value }"
-            @click="selectOption(item)"
-          >
+          <div class="menu-item" v-for="item in options" :key="item.value"
+            :class="{ 'is-active': value === item.value }" @click="selectOption(item)">
             {{ item.label }}
           </div>
         </div>
@@ -32,36 +24,48 @@
 <script>
 export default {
   name: 'DropdownSelect',
-  model: {
-    prop: 'value',
-    event: 'change'
-  },
+  model: { prop: 'value', event: 'change' },
   props: {
-    size: {
-      type: String,
-      default: 'normal'
-    },
-    value: {
-      type: [String, Number],
-      default: ''
-    },
-    options: {
-      type: Array,
-      default: () => []
-    },
-    placeholder: {
-      type: String,
-      default: '请选择'
-    },
-    // 【新增】主题风格:'bordered' (透明边框) | 'solid' (实心深色背景)
+    /**
+     * 尺寸模式
+     * 'normal' - 固定宽度 | 'auto' - 随文字自适应
+     */
+    size: { type: String, default: 'normal' },
+
+    /** 绑定值 */
+    value: { type: [String, Number], default: '' },
+
+    /** 选项列表 [{label: '30s', value: 30}] */
+    options: { type: Array, default: () => [] },
+
+    /** 未选择时的占位符 */
+    placeholder: { type: String, default: '请选择' },
+
+    /**
+     * 主题风格选择
+     * 'bordered' - 【经典版】白色背景,浅蓝色边框,适合常规表单页面。
+     * 'solid'    - 【卡片版】深蓝灰色背景,无边框,完美契合你当前的路口控制卡片。
+     * 'neon'     - 【极客版】新增设计,全透明背景配合青色霓虹发光边框,适合高亮科技感大屏。
+     */
     theme: {
       type: String,
-      default: 'bordered'
-    }
+      default: 'bordered',
+    },
+
+    /** 是否将下拉列表挂载至 body (解决 Swiper 等容器 overflow:hidden 导致的遮挡问题) */
+    appendToBody: { type: Boolean, default: true }
   },
   data() {
     return {
-      isOpen: false
+      isOpen: false,
+      closeTimer: null,
+      menuStyle: {
+        position: 'absolute',
+        top: '0px',
+        left: '0px',
+        zIndex: 9999,
+        width: 'auto'
+      }
     };
   },
   computed: {
@@ -70,25 +74,69 @@ export default {
       return selected ? selected.label : this.placeholder;
     }
   },
+  watch: {
+    isOpen(val) {
+      if (val) {
+        this.$nextTick(() => {
+          this.updatePosition();
+          if (this.appendToBody && this.$refs.menu) {
+            document.body.appendChild(this.$refs.menu);
+          }
+        });
+      }
+    }
+  },
   mounted() {
     document.addEventListener('click', this.handleClickOutside);
+    window.addEventListener('resize', this.updatePosition);
+    window.addEventListener('scroll', this.updatePosition, true);
   },
   beforeDestroy() {
     document.removeEventListener('click', this.handleClickOutside);
+    window.removeEventListener('resize', this.updatePosition);
+    window.removeEventListener('scroll', this.updatePosition, true);
+    if (this.closeTimer) clearTimeout(this.closeTimer);
+    if (this.appendToBody && this.$refs.menu && this.$refs.menu.parentNode === document.body) {
+      document.body.removeChild(this.$refs.menu);
+    }
   },
   methods: {
-    toggleDropdown() {
-      this.isOpen = !this.isOpen;
+    toggleDropdown() { this.isOpen = !this.isOpen; },
+    handleMouseEnter() { if (this.closeTimer) clearTimeout(this.closeTimer); },
+    handleMouseLeave() {
+      this.closeTimer = setTimeout(() => { this.isOpen = false; }, 250);
     },
-    selectOption(item) {
-      if (this.value !== item.value) {
-        this.$emit('change', item.value); 
-        this.$emit('select', item);       
+    updatePosition() {
+      if (!this.isOpen || !this.$refs.dropdown || !this.$refs.menu) return;
+      const triggerRect = this.$refs.dropdown.getBoundingClientRect();
+      const menuHeight = this.$refs.menu.offsetHeight || 200;
+      const viewportHeight = window.innerHeight;
+
+      let top = triggerRect.bottom + window.scrollY + 8;
+      let isTop = false;
+
+      // 空间探测:若下方空间不足且上方充足,则翻转
+      if (triggerRect.bottom + menuHeight > viewportHeight && triggerRect.top > menuHeight) {
+        top = triggerRect.top + window.scrollY - menuHeight - 8;
+        isTop = true;
       }
+
+      this.menuStyle = {
+        position: 'absolute',
+        top: `${top}px`,
+        left: `${triggerRect.left + window.scrollX}px`,
+        width: `${triggerRect.width}px`,
+        zIndex: 9999
+      };
+      this.$refs.menu.setAttribute('data-placement', isTop ? 'top' : 'bottom');
+    },
+    selectOption(item) {
+      this.$emit('change', item.value);
       this.isOpen = false;
     },
     handleClickOutside(event) {
-      if (this.$refs.dropdown && !this.$refs.dropdown.contains(event.target)) {
+      if (this.$refs.dropdown && !this.$refs.dropdown.contains(event.target) &&
+        this.$refs.menu && !this.$refs.menu.contains(event.target)) {
         this.isOpen = false;
       }
     }
@@ -97,163 +145,201 @@ export default {
 </script>
 
 <style scoped>
-/* 最外层容器 */
+/* ================== 核心容器与公共样式 ================== */
 .custom-dropdown {
   position: relative;
   display: inline-block;
   user-select: none;
 }
 
-/* --- 触发器按钮 (基础样式) --- */
 .dropdown-trigger {
   display: flex;
   align-items: center;
   justify-content: space-between;
   padding: 6px 12px;
   min-width: 80px;
-  color: #ffffff;
+  color: #fff;
   font-size: 14px;
   cursor: pointer;
   transition: all 0.2s;
 }
-.trigger-text {
-  margin-right: 12px;
-}
+
 .arrow-icon {
   width: 0;
   height: 0;
   border-left: 4px solid transparent;
   border-right: 4px solid transparent;
-  border-top: 5px solid #ffffff;
-  transition: transform 0.3s ease;
+  border-top: 5px solid #fff;
+  transition: transform 0.3s;
 }
-.dropdown-trigger.is-open .arrow-icon {
+
+.is-open .arrow-icon {
   transform: rotate(180deg);
 }
 
-/* ================== 主题 1:透明边框 (默认原样式) ================== */
+/* ================== 主题样式表 ================== */
+
+/* 1. Bordered 主题 (白色) */
 .theme-bordered .dropdown-trigger {
-  background-color: transparent;
-  border: 1px solid rgba(100, 130, 190, 0.6); 
+  background: transparent;
+  border: 1px solid rgba(100, 130, 190, 0.6);
   border-radius: 2px;
 }
-.theme-bordered .dropdown-trigger:hover,
-.theme-bordered .dropdown-trigger.is-open {
-  border-color: rgba(140, 180, 255, 0.9);
+
+.dropdown-menu.theme-bordered {
+  background: #fff;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.theme-bordered .menu-item {
+  color: #333;
+}
+
+.theme-bordered .menu-item:hover {
+  background: #f0f5ff;
+  color: #4da8ff;
+}
+
+.theme-bordered .menu-item.is-active {
+  color: #4da8ff;
+  font-weight: bold;
+}
+
+.theme-bordered[data-placement="bottom"] .menu-arrow {
+  border-bottom: 6px solid #fff;
+  top: -5px;
+}
+
+.theme-bordered[data-placement="top"] .menu-arrow {
+  border-top: 6px solid #fff;
+  bottom: -5px;
 }
 
-/* ================== 主题 2:实心深色 (还原你的最新截图) ================== */
+/* 2. Solid 主题 (深蓝灰 - 你目前的风格) */
 .theme-solid .dropdown-trigger {
-  background-color: #273444;; /* 截图中的深蓝色底色 */
-  border: 1px solid transparent; /* 占位防止跳动 */
-  border-radius: 4px; /* 稍微圆润的边角 */
+  background: #273444;
+  border-radius: 4px;
   padding: 5px 8px;
 }
-.theme-solid .dropdown-trigger:hover,
-.theme-solid .dropdown-trigger.is-open {
-  background-color: #385180; /* hover 时稍微提亮 */
+
+.theme-solid .dropdown-trigger:hover {
+  background: #385180;
+}
+
+.dropdown-menu.theme-solid {
+  background: #1e2c4a;
+  border: 1px solid rgba(100, 130, 190, 0.3);
 }
 
+.theme-solid .menu-item {
+  color: #c4d7f0;
+}
 
-/* --- 下拉菜单容器 (基础位置与动画) --- */
+.theme-solid .menu-item:hover {
+  background: #2b3f66;
+  color: #32F6F8;
+}
+
+.theme-solid .menu-item.is-active {
+  color: #32F6F8;
+  font-weight: bold;
+}
+
+.theme-solid[data-placement="bottom"] .menu-arrow {
+  border-bottom: 6px solid #1e2c4a;
+  top: -6px;
+}
+
+.theme-solid[data-placement="top"] .menu-arrow {
+  border-top: 6px solid #1e2c4a;
+  bottom: -6px;
+}
+
+/* 3. Neon 主题 (极客霓虹 - 科技蓝版) */
+.theme-neon .dropdown-trigger { 
+  background: rgba(68, 138, 255, 0.05); 
+  border: 1px solid rgba(68, 138, 255, 0.3); 
+  border-radius: 20px; /* 圆角胶囊感 */
+  box-shadow: inset 0 0 8px rgba(68, 138, 255, 0.1);
+}
+.theme-neon .dropdown-trigger:hover,
+.theme-neon .dropdown-trigger.is-open { 
+  border-color: #448AFF; 
+  box-shadow: 0 0 10px rgba(68, 138, 255, 0.4); 
+}
+.dropdown-menu.theme-neon { 
+  background: rgba(10, 20, 40, 0.95); 
+  backdrop-filter: blur(10px); /* 磨砂玻璃效果 */
+  border: 1px solid #448AFF;
+  box-shadow: 0 0 20px rgba(68, 138, 255, 0.2);
+}
+.theme-neon .menu-item { color: rgba(68, 138, 255, 0.7); }
+.theme-neon .menu-item:hover { 
+  background: rgba(68, 138, 255, 0.15); 
+  color: #fff; 
+  text-shadow: 0 0 5px #448AFF; 
+}
+.theme-neon .menu-item.is-active { 
+  color: #fff; 
+  background: rgba(68, 138, 255, 0.25); 
+}
+.theme-neon[data-placement="bottom"] .menu-arrow { 
+  border-bottom: 6px solid #448AFF; 
+  top: -5px; 
+}
+.theme-neon[data-placement="top"] .menu-arrow { 
+  border-top: 6px solid #448AFF; 
+  bottom: -5px; 
+}
+
+/* ================== 通用功能样式 ================== */
 .dropdown-menu {
-  position: absolute;
-  top: calc(100% + 10px);
-  left: 50%;
-  transform: translateX(-50%);
-  min-width: 100%; /* 至少与触发器同宽 */
   border-radius: 6px;
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
-  z-index: 1000; 
+  position: fixed;
+  box-sizing: border-box;
 }
+
+/* 使用 fixed 配合 js 计算 */
 .menu-arrow {
   position: absolute;
-  top: -5px; 
   left: 50%;
   transform: translateX(-50%);
   width: 0;
   height: 0;
   border-left: 6px solid transparent;
   border-right: 6px solid transparent;
+  z-index: 10;
 }
+
 .menu-list {
   padding: 6px 0;
+  max-height: 200px;
+  overflow-y: auto;
 }
+
 .menu-item {
   padding: 8px 16px;
   font-size: 14px;
   text-align: center;
   cursor: pointer;
-  transition: all 0.2s;
-}
-
-/* ================== 下拉菜单配色:主题 1 (白色气泡) ================== */
-.theme-bordered .dropdown-menu {
-  background-color: #ffffff;
-}
-.theme-bordered .menu-arrow {
-  border-bottom: 6px solid #ffffff;
-}
-.theme-bordered .menu-item {
-  color: #333333;
-}
-.theme-bordered .menu-item:hover {
-  background-color: #f0f5ff;
-  color: #4da8ff;
-}
-.theme-bordered .menu-item.is-active {
-  color: #4da8ff;
-  font-weight: bold;
-}
-
-/* ================== 下拉菜单配色:主题 2 (深色科技气泡) ================== */
-.theme-solid .dropdown-menu {
-  background-color: #1e2c4a;
-  border: 1px solid rgba(100, 130, 190, 0.3);
-}
-.theme-solid .menu-arrow {
-  border-bottom: 6px solid #1e2c4a;
-}
-/* 利用伪元素单独画暗色三角形边框,防止悬浮感不足 */
-.theme-solid .menu-arrow::before {
-  content: '';
-  position: absolute;
-  top: 1px;
-  left: -6px;
-  border-left: 6px solid transparent;
-  border-right: 6px solid transparent;
-  border-bottom: 6px solid rgba(100, 130, 190, 0.3);
-  z-index: -1;
-}
-.theme-solid .menu-item {
-  color: #c4d7f0;
-}
-.theme-solid .menu-item:hover {
-  background-color: #2b3f66;
-  color: #32F6F8;
-}
-.theme-solid .menu-item.is-active {
-  color: #32F6F8;
-  font-weight: bold;
+  transition: 0.2s;
+  user-select: none;
 }
 
-/* Vue 过渡动画 */
-.fade-enter-active, .fade-leave-active {
+.fade-enter-active,
+.fade-leave-active {
   transition: opacity 0.2s, transform 0.2s;
 }
-.fade-enter, .fade-leave-to {
+
+.fade-enter,
+.fade-leave-to {
   opacity: 0;
-  transform: translate(-50%, -5px);
+  transform: translateY(-5px);
 }
 
-/* 自适应模式:继承父级 font-size */
 .size-auto .dropdown-trigger {
   padding: 0.4em 0.8em;
   min-width: 5em;
   font-size: inherit;
 }
-.size-auto .menu-item {
-  padding: 0.5em 1em;
-  font-size: inherit;
-}
 </style>

+ 23 - 3
src/components/ui/IntersectionControlCard.vue

@@ -14,6 +14,9 @@
                 <div class="info-item">驻留阶段:<span>{{ data.stage }}</span></div>
                 <div class="info-item">执行方式:<span>{{ data.mode }}</span></div>
                 <div class="info-item">剩余时间:<span class="time">{{ data.timeLeft }}s</span></div>
+                <div class="info-item lock-time-row">
+                    锁定时间:<DropdownSelect v-model="lockTime" :options="lockTimeOptions" size="auto" theme="neon" />
+                </div>
                 <button 
                     @click="$emit('action-click', data)"
                     :class="{'btn btn-view margin-top-auto': data.btnType === 'normal', 'action-btn primary': data.btnType === 'primary'}">
@@ -38,12 +41,22 @@
 
 <script>
 import IntersectionMap from '@/components/ui/IntersectionMap.vue';
+import DropdownSelect from '@/components/ui/DropdownSelect.vue';
 
 export default {
     name: 'IntersectionControlCard',
-    components: { IntersectionMap },
-    props: { 
-        data: { type: Object, required: true } 
+    components: { IntersectionMap, DropdownSelect },
+    props: {
+        data: { type: Object, required: true }
+    },
+    data() {
+        return {
+            lockTime: 30,
+            lockTimeOptions: Array.from({ length: 10 }, (_, i) => {
+                const val = (i + 1) * 30;
+                return { label: val + 's', value: val };
+            }),
+        };
     },
     methods: {
         // 单选逻辑处理
@@ -228,6 +241,13 @@ export default {
 .phase-box.is-active::after {
     opacity: 1; 
 }
+.lock-time-row {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    white-space: nowrap;
+}
+
 .margin-top-auto {
     margin-top: auto;
 }

+ 0 - 5
src/components/ui/SpecialTaskMonitorPanel.vue

@@ -16,7 +16,6 @@
           <IntersectionControlCard 
             v-if="item.card" 
             :data="item.card" 
-            class="margin-top-20"
             @action-click="handleCardAction"
           />
         </div>
@@ -160,10 +159,6 @@ export default {
   box-sizing: border-box;
 }
 
-.margin-top-20 {
-  margin-top: 20px;
-}
-
 /* ================= 自定义左右箭头样式 ================= */
 .swiper-button-prev:after, .swiper-button-next:after { display: none; } /* 隐藏默认箭头 */