|
|
@@ -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>
|