Kaynağa Gözat

feat(组件): 添加BetterSelect下拉选择组件

实现一个增强版的下拉选择组件,包含以下功能:
- 支持双向数据绑定
- 点击外部区域自动关闭选项列表
- 响应式调整选项列表位置
- 支持自定义宽度和占位符
- 添加过渡动画效果
Sakulin 2 ay önce
ebeveyn
işleme
ab0d99ffe0
1 değiştirilmiş dosya ile 162 ekleme ve 0 silme
  1. 162 0
      src/components/BetterSelect.vue

+ 162 - 0
src/components/BetterSelect.vue

@@ -0,0 +1,162 @@
+<script setup lang="ts">
+import { ref, computed, onMounted, ref as elementRef, onUnmounted, watch } from 'vue';
+
+const props = defineProps<{
+  options: { value: string; label: string }[];
+  modelValue: string;
+  placeholder: string;
+  width: string;
+}>();
+
+const emit = defineEmits(['update:modelValue']);
+
+const showOptions = ref(false);
+const selectedValueRef = elementRef<HTMLElement | null>(null);
+const optionsListRef = elementRef<HTMLElement | null>(null);
+
+const handleClickOutside = (event: MouseEvent) => {
+  if (selectedValueRef.value && optionsListRef.value && showOptions.value) {
+    const isClickInside = selectedValueRef.value.contains(event.target as Node) || optionsListRef.value.contains(event.target as Node);
+    if (!isClickInside) {
+      showOptions.value = false;
+    }
+  }
+};
+
+const handleSelect = (option: { value: string; label: string }) => {
+  emit('update:modelValue', option.value);
+  showOptions.value = false;
+};
+
+const selectedOption = computed(() => {
+  return props.options.find(option => option.value === props.modelValue);
+});
+
+let updateTimer: number | null = null;
+
+const updateOptionsListPosition = () => {
+  if (updateTimer) {
+    clearTimeout(updateTimer);
+  }
+
+  updateTimer = setTimeout(() => {
+    if (selectedValueRef.value && optionsListRef.value && showOptions.value) {
+      optionsListRef.value.style.top = selectedValueRef.value.getBoundingClientRect().bottom + 'px';
+      optionsListRef.value.style.left = selectedValueRef.value.getBoundingClientRect().left + 'px';
+      optionsListRef.value.style.width = selectedValueRef.value.offsetWidth + 'px';
+    }
+    updateTimer = null;
+  }, 300);
+};
+
+onMounted(() => {
+  document.addEventListener('click', handleClickOutside);
+  window.addEventListener('resize', updateOptionsListPosition);
+});
+
+onUnmounted(() => {
+  document.removeEventListener('click', handleClickOutside);
+  window.removeEventListener('resize', updateOptionsListPosition);
+});
+
+watch(showOptions, (newValue) => {
+  if (newValue) {
+    updateOptionsListPosition();
+  }
+});
+
+</script>
+
+<template>
+  <div class="better-select" :class="showOptions ? ['showing-select'] : []">
+    <div ref="selectedValueRef" :class="selectedOption ? [] : ['placeholder-showing']" class="selected-value" @click="showOptions = !showOptions">
+      {{ selectedOption?.label || props.placeholder }}
+    </div>
+    <ul
+      ref="optionsListRef"
+      class="options-list"
+      :class="showOptions ? ['showing-options-list'] : []"
+      :style="{
+        position: 'fixed',
+        top: selectedValueRef?.getBoundingClientRect().bottom + 'px',
+        left: selectedValueRef?.getBoundingClientRect().left + 'px',
+        width: selectedValueRef?.offsetWidth + 'px'
+      }"
+    >
+      <li
+        v-for="option in props.options"
+        :key="option.value"
+        :class="option.value == selectedOption?.value ? ['active'] : []"
+        @click="handleSelect(option)"
+      >
+        {{ option.label }}
+      </li>
+    </ul>
+  </div>
+</template>
+
+<style scoped>
+.better-select {
+  position: relative;
+  text-align: center;
+  width: v-bind("props.width");
+}
+
+.selected-value:hover {
+  background-size: 100% 2px;
+}
+
+.showing-select .selected-value {
+  background-size: 100% 2px;
+}
+
+.selected-value {
+  padding: 2px;
+  cursor: pointer;
+  background: linear-gradient(to right, var(--text-color), var(--text-color)) no-repeat left bottom;
+  color: var(--text-color);
+  background-size: 0 2px;
+  transition: all .3s;
+}
+
+.placeholder-showing {
+  color: var(--secondary-text-color);
+}
+
+.options-list {
+  margin: 0;
+  padding: 0;
+  opacity: 0;
+  list-style-type: none;
+  border-top: none;
+  border-radius: 0 0 4px 4px;
+  max-height: 200px;
+  overflow-y: hidden;
+  background-color: var(--background-color);
+  z-index: 1000;
+  transform-origin: top;
+  transform: scaleY(0%);
+  transition: all .3s;
+}
+
+.showing-options-list {
+  opacity: 100%;
+  transform: scaleY(100%);
+  box-shadow: var(--muted-text-color) 0 0 4px;
+}
+
+.options-list li {
+  padding: 4px 0;
+  cursor: pointer;
+  text-align: center;
+  transition: all .3s;
+}
+
+.options-list .active {
+  background-color: var(--secondary-background-color);
+}
+
+.options-list li:hover {
+  background-color: var(--secondary-background-color);
+}
+</style>