Vue:自定义slider滑块组件

发布于:2025-08-19 ⋅ 阅读:(19) ⋅ 点赞:(0)

在这里插入图片描述

<template>
  <div class="custom-slider-container" :style="{ height: height }">
    <div class="custom-slider-vertical" v-if="layout === 'vertical'">
      <t-input-number
        v-model="inputValue"
        :max="max"
        :min="min"
        :theme="inputTheme"
        class="slider-input"
        @blur="handleInputBlur"
        @input="handleInputChange"
      />
      <div class="slider-track" ref="track" @click="handleTrackClick">
        <div class="slider-fill" :style="{ height: fillPercentage }"></div>
        <div 
          class="slider-thumb" 
          :style="{ bottom: fillPercentage }"
          @mousedown="startDrag"
        ></div>
      </div>
    </div>
    
    <div class="custom-slider-horizontal" v-else>
      <div class="slider-track-wrapper">
        <div class="slider-track" ref="track" @click="handleTrackClick">
          <div class="slider-fill" :style="{ width: fillPercentage }"></div>
          <div 
            class="slider-thumb" 
            :style="{ left: fillPercentage }"
            @mousedown="startDrag"
          ></div>
        </div>
      </div>
      <div class="slider-input-wrapper">
        <t-input-number
          v-model="inputValue"
          :max="max"
          :min="min"
          :theme="inputTheme"
          class="slider-input"
          @blur="handleInputBlur"
          @input="handleInputChange"
        />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'CustomSlider',
  props: {
    value: {
      type: Number,
      default: 15
    },
    min: {
      type: Number,
      default: 0
    },
    max: {
      type: Number,
      default: 100
    },
    layout: {
      type: String,
      default: 'horizontal',
      validator: value => ['horizontal', 'vertical'].includes(value)
    },
    height: {
      type: String,
      default: '200px'
    },
    inputTheme: {
      type: String,
      default: 'column'
    },
    showTooltip: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      inputValue: this.value,
      isDragging: false,
      tempInputValue: '', // 临时存储输入值
      displayValue: this.value // 用于显示的值,始终在min和max之间
    };
  },
  computed: {
    fillPercentage() {
      // 始终使用displayValue计算百分比,确保滑块位置正确
      const percentage = ((this.displayValue - this.min) / (this.max - this.min)) * 100;
      return `${Math.max(0, Math.min(100, percentage))}%`;
    }
  },
  watch: {
    value(newVal) {
      // 当外部值变化时更新内部值
      this.inputValue = newVal;
      this.displayValue = newVal;
    }
  },
  mounted() {
    // 添加全局事件监听
    document.addEventListener('mousemove', this.handleDrag);
    document.addEventListener('mouseup', this.stopDrag);
  },
  beforeDestroy() {
    // 移除全局事件监听
    document.removeEventListener('mousemove', this.handleDrag);
    document.removeEventListener('mouseup', this.stopDrag);
  },
  methods: {
    handleInputBlur() {
      // 输入框失去焦点时,验证并更新值
      this.validateAndUpdateInput();
    },
    handleInputChange(value) {
      // 输入框值变化时,存储临时值但不立即验证
      this.tempInputValue = String(value);
      
      // 同时更新inputValue,但限制displayValue在min和max之间
      this.inputValue = value;
      
      // 计算临时显示值,但不更新到实际值
      const numValue = parseInt(value, 10);
      if (!isNaN(numValue)) {
        // 临时显示值,但不超出范围
        this.displayValue = Math.max(this.min, Math.min(this.max, numValue));
      }
    },
    validateAndUpdateInput() {
      // 验证并更新输入值
      if (this.tempInputValue === '') {
        this.inputValue = this.min;
        this.displayValue = this.min;
        this.$emit('input', this.min);
        this.$emit('change', this.min);
        return;
      }
      
      const numValue = parseInt(this.tempInputValue, 10);
      let finalValue;
      
      if (isNaN(numValue)) {
        finalValue = this.min;
      } else if (numValue < this.min) {
        finalValue = this.min;
      } else if (numValue > this.max) {
        finalValue = this.max;
      } else {
        finalValue = numValue;
      }
      
      // 更新所有值
      this.inputValue = finalValue;
      this.displayValue = finalValue;
      
      // 清除临时值
      this.tempInputValue = '';
      
      // 通知父组件值已更改
      this.$emit('input', finalValue);
      this.$emit('change', finalValue);
    },
    startDrag(event) {
      this.isDragging = true;
      event.preventDefault();
    },
    stopDrag() {
      if (this.isDragging) {
        this.isDragging = false;
        // 通知父组件值已更改
        this.$emit('change', this.displayValue);
      }
    },
    handleDrag(event) {
      if (!this.isDragging) return;
      
      const track = this.$refs.track;
      const rect = track.getBoundingClientRect();
      
      let percentage;
      if (this.layout === 'vertical') {
        // 垂直滑块计算
        const position = rect.bottom - event.clientY;
        percentage = Math.max(0, Math.min(1, position / rect.height));
      } else {
        // 水平滑块计算
        const position = event.clientX - rect.left;
        percentage = Math.max(0, Math.min(1, position / rect.width));
      }
      
      // 计算新值
      const newValue = Math.round(this.min + percentage * (this.max - this.min));
      this.inputValue = newValue;
      this.displayValue = newValue;
      
      // 通知父组件值正在变化
      this.$emit('input', newValue);
    },
    handleTrackClick(event) {
      // 点击轨道时移动滑块
      const track = this.$refs.track;
      const rect = track.getBoundingClientRect();
      
      let percentage;
      if (this.layout === 'vertical') {
        // 垂直滑块计算
        const position = rect.bottom - event.clientY;
        percentage = Math.max(0, Math.min(1, position / rect.height));
      } else {
        // 水平滑块计算
        const position = event.clientX - rect.left;
        percentage = Math.max(0, Math.min(1, position / rect.width));
      }
      
      // 计算新值
      const newValue = Math.round(this.min + percentage * (this.max - this.min));
      this.inputValue = newValue;
      this.displayValue = newValue;
      
      // 通知父组件值已更改
      this.$emit('input', newValue);
      this.$emit('change', newValue);
    }
  }
};
</script>

<style scoped>
.custom-slider-container {
  display: flex;
  align-items: center;
  justify-content: center;
}

.custom-slider-vertical {
  display: flex;
  flex-direction: column;
  align-items: center;
  height: 100%;
}

.custom-slider-horizontal {
  display: flex;
  flex-direction: row;
  align-items: center;
  width: 100%;
}

.slider-track-wrapper {
  flex: 1; /* 占据剩余空间 */
  min-width: 0; /* 允许收缩 */
}

.slider-input-wrapper {
  width: 80px; /* 固定输入框宽度 */
  flex-shrink: 0; /* 防止收缩 */
  margin-left: 10px;
}

.slider-track {
  position: relative;
  background-color: #e9ecef;
  border-radius: 4px;
}

.custom-slider-vertical .slider-track {
  width: 6px;
  height: 100%;
  margin: 0 10px;
}

.custom-slider-horizontal .slider-track {
  height: 6px;
  width: 100%;
  margin: 10px 0;
}

.slider-fill {
  position: absolute;
  background-color: #0052d9;
  border-radius: 4px;
}

.custom-slider-vertical .slider-fill {
  width: 100%;
  bottom: 0;
}

.custom-slider-horizontal .slider-fill {
  height: 100%;
  left: 0;
}

.slider-thumb {
  position: absolute;
  width: 16px;
  height: 16px;
  background-color: #fff;
  border: 2px solid #0052d9;
  border-radius: 50%;
  cursor: pointer;
  z-index: 2;
  transition: none; /* 移除过渡效果,使滑块移动更精确 */
}

.custom-slider-vertical .slider-thumb {
  left: 50%;
  transform: translateX(-50%) translateY(50%); /* 垂直滑块居中并向下偏移 */
}

.custom-slider-horizontal .slider-thumb {
  top: 50%;
  transform: translateY(-50%) translateX(-50%); /* 水平滑块居中并向左偏移 */
}

.slider-input {
  width: 100%; /* 输入框占满容器宽度 */
}
</style>


网站公告

今日签到

点亮在社区的每一天
去签到