Vue3滑动输入条(Slider)

Vue2滑动输入条(Slider)

可自定义设置以下属性:

  • 滑动输入条的宽度(width),类型:string|number,默认 '100%'

  • 滑动输入条最小值(min),类型:number,默认 0

  • 滑动输入条最大值(max),类型:number,默认 100

  • 是否禁用(disabled),类型:boolean,默认 false

  • 是否双滑块模式(range),类型:boolean,默认 false

  • 步长,取值必须大于0,并且可被 (max - min) 整除(step),类型:number,默认 1

  • 设置当前取值,当 range 为 false 时,使用 number,否则用 [number, number](v-model: value),类型:number|number[],默认 0

主要具有以下功能:

  • 点击滑动输入条直接调转到指定位置,并获取当前数值

  • 可分别拖动左右滑块改变当前数值

  • 点击聚焦任一滑块,然后使用键盘上(up)、下(down)、左(left)、右(right)、箭头改变数值

效果如下图:

 ①创建滑动输入条组件Slider.vue:

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { rafTimeout, cancelRaf } from '../index'
interface Props {
  width?: string|number // 滑动输入条的宽度
  min?: number // 滑动输入条最小值
  max?: number // 滑动输入条最大值
  disabled?: boolean // 是否禁用
  range?: boolean // 是否双滑块模式
  step?: number // 步长,取值必须大于0,并且可被 (max - min) 整除
  value?: number | number[] // (v-model)设置当前取值,当 range 为 false 时,使用 number,否则用 [number, number]
}
const props = withDefaults(defineProps<Props>(), {
  width: '100%',
  min: 0,
  max: 100,
  disabled: false,
  range: false,
  step: 1,
  value: 0
})
const transition = ref(false)
const timer = ref()
const left = ref(0) // 左滑块距离滑动条左端的距离
const right = ref(0) // 右滑动距离滑动条左端的距离
const slider = ref()
const sliderWidth = ref()
const leftHandle = ref() // left模板引用
const rightHandle = ref() // right模板引用

const scale = computed(() => {
  return sliderWidth.value / (props.max - props.min)
})
const totalWidth = computed(() => {
  if (typeof props.width === 'number') {
    return props.width + 'px'
  } else {
    return props.width
  }
})
const sliderValue = computed(() => {
  const high = Math.round(right.value / scale.value + props.min)
  if (props.range) {
    const low = Math.round(left.value / scale.value + props.min)
    return [low, high]
  }
  return high
})
const emit = defineEmits(['update:value', 'change'])
watch(
  () => props.value,
  () => { getPosition() })
watch(sliderValue, (to) => {
  emit('update:value', to)
  emit('change', to)
})
onMounted(() => {
  getSliderWidth()
  getPosition()
})
function getSliderWidth () {
  sliderWidth.value = slider.value.offsetWidth
}
function getPosition () {
  if (props.range) { // 双滑块模式
    left.value = ((props.value as number[])[0] - props.min) * scale.value
    right.value = ((props.value as number[])[1] - props.min) * scale.value
  } else {
    right.value = (props.value as number - props.min) * scale.value
  }
}
function onClickPoint (e: any) { // 点击滑动条,移动滑块
  if (transition.value) {
    cancelRaf(timer.value)
    timer.value = null
  } else {
    transition.value = true
  }
  timer.value = rafTimeout(() => {
    transition.value = false
  }, 300)
  // 元素是absolute时,e.layerX是相对于自身元素左上角的水平位置
  const targetX = e.layerX // 鼠标点击位置距离滑动输入条左端的水平距离
  if (props.range) { // 双滑块模式
    if (targetX <= left.value) {
      left.value = targetX
    } else if (targetX >= right.value) {
      right.value = targetX
    } else {
      if ((targetX - left.value) < (right.value - targetX)) {
        left.value = targetX
      } else {
        right.value = targetX
      }
    }
  } else { // 单滑块模式
    right.value = targetX
  }
}
function onLeftMouseDown () { // 在滚动条上拖动左滑块
  const leftX = slider.value.getBoundingClientRect().left // 滑动条左端距离屏幕可视区域左边界的距离
  document.onmousemove = (e: MouseEvent) => {
    // e.clientX返回事件被触发时鼠标指针相对于浏览器可视窗口的水平坐标
    const targetX = e.clientX - leftX
    if (targetX < 0) {
      left.value = 0
    } else if (targetX >= 0 && targetX <= right.value) {
      left.value = targetX
    } else { // targetX > right
      left.value = right.value
      onRightMouseDown()
    }
  }
  document.onmouseup = () => {
    document.onmousemove = null
  }
}
function onRightMouseDown () { // 在滚动条上拖动右滑块
  const leftX = slider.value.getBoundingClientRect().left // 滑动条左端距离屏幕可视区域左边界的距离
  document.onmousemove = (e: MouseEvent) => {
    // e.clientX返回事件被触发时鼠标指针相对于浏览器可视窗口的水平坐标
    const targetX = e.clientX - leftX
    if (targetX > sliderWidth.value) {
      right.value = sliderWidth.value
    } else if (left.value <= targetX && targetX <= sliderWidth.value) {
      right.value = targetX
    } else { // targetX < left
      right.value = left.value
      onLeftMouseDown()
    }
  }
  document.onmouseup = () => {
    document.onmousemove = null
  }
}
function onLeftSlide (source: number, place: string) {
  const targetX = source - props.step * scale.value
  if (place === 'left') { // 左滑块左移
    if (targetX < 0) {
      left.value = 0
    } else {
      left.value = targetX
    }
  } else { // 右滑块左移
    if (targetX >= left.value) {
      right.value = targetX
    } else {
      right.value = left.value
      left.value = targetX
      leftHandle.value.focus()
    }
  }
}
function onRightSlide (source: number, place: string) {
  const targetX = source + props.step * scale.value
  if (place === 'right') { // 右滑块右移
    if (targetX > sliderWidth.value) {
      right.value = sliderWidth.value
    } else {
      right.value = targetX
    }
  } else { // 左滑块右移
    if (targetX <= right.value) {
      left.value = targetX
    } else {
      left.value = right.value
      right.value = targetX
      rightHandle.value.focus()
    }
  }
}
</script>
<template>
  <div :class="['m-slider', { disabled: disabled }]" ref="slider" :style="`width: ${totalWidth};`">
    <div class="u-slider-rail" @click.self="onClickPoint"></div>
    <div class="u-slider-track" :class="{trackTransition: transition}" :style="`left: ${left}px; right: auto; width: ${right - left}px;`"></div>
    <div
      v-if="range"
      tabindex="-1"
      ref="leftHandle"
      class="u-slider-handle"
      :class="{handleTransition: transition}"
      :style="`left: ${left}px; right: auto; transform: translateX(-50%);`"
      @keydown.left.prevent="onLeftSlide(left, 'left')"
      @keydown.right.prevent="onRightSlide(left, 'left')"
      @keydown.down.prevent="onLeftSlide(left, 'left')"
      @keydown.up.prevent="onRightSlide(left, 'left')"
      @mousedown="onLeftMouseDown"></div>
    <div
      tabindex="-1"
      ref="rightHandle"
      class="u-slider-handle"
      :class="{handleTransition: transition}"
      :style="`left: ${right}px; right: auto; transform: translateX(-50%);`"
      @keydown.left.prevent="onLeftSlide(right, 'right')"
      @keydown.right.prevent="onRightSlide(right, 'right')"
      @keydown.down.prevent="onLeftSlide(right, 'right')"
      @keydown.up.prevent="onRightSlide(right, 'right')"
      @mousedown="onRightMouseDown"
      ></div>
  </div>
</template>
<style lang="less" scoped>
.m-slider {
  display: inline-block;
  height: 4px;
  position: relative;
  z-index: 9;
  touch-action: none; // 禁用元素上的所有手势,使用自己的拖动和缩放api
  .u-slider-rail { // 灰色未选择滑动条背景色
    position: absolute;
    z-index: 99;
    height: 4px;
    width: 100%;
    background-color: #f5f5f5;
    border-radius: 2px;
    cursor: pointer;
    transition: background-color .3s;
  }
  .u-slider-track { // 蓝色已选择滑动条背景色
    position: absolute;
    z-index: 99;
    background: lighten(fade(@themeColor, 54%), 10%);
    border-radius: 4px;
    height: 4px;
    cursor: pointer;
    transition: background .3s;
    pointer-events: none;
  }
  .trackTransition {
    transition: left .2s, width .2s, background .3s;
  }
  &:hover {
    .u-slider-rail { // 灰色未选择滑动条背景色
      background: #E3E3E3;
    }
    .u-slider-track { // 蓝色已选择滑动条背景色
      background: @themeColor;
    }
  }
  .u-slider-handle { // 滑块
    position: absolute;
    z-index: 999;
    width: 12px;
    height: 12px;
    top: -6px;
    background: #fff;
    border: 2px solid lighten(fade(@themeColor, 54%), 10%);
    border-radius: 50%;
    cursor: pointer;
    transition: border-color .3s,box-shadow .6s,transform .3s cubic-bezier(.18,.89,.32,1.28);
    &:focus {
      border-color: @themeColor;
      box-shadow: 0 0 0 5px fade(@themeColor, 20%);
    }
    &:hover {
      border-color: @themeColor;
    }
  }
  .handleTransition {
    transition: left .2s, border-color .3s,box-shadow .6s,transform .3s cubic-bezier(.18,.89,.32,1.28);
  }
}
.disabled {
  cursor: not-allowed;
  .u-slider-rail {
    pointer-events: none;
  }
  .u-slider-track {
    background: rgba(0, 0, 0, .25);
  }
  .u-slider-handle {
    border-color: rgba(0, 0, 0, .25);
    pointer-events: none;
  }
  &:hover {
    .u-slider-track {
       background: rgba(0, 0, 0, .25);
    }
  }
}
</style>

②在要使用的页面引入:

<script setup lang="ts">
import { Progress } from './Progress.vue'
import { ref, watch } from 'vue'

const singleValue = ref(20)
const doubleValue = ref([20, 80])
watch(singleValue, (to) => {
  console.log('p to:', to)
})
watch(doubleValue, (to) => {
  console.log('p to:', to)
})
function onChange (val: number|number[]) {
  console.log('change:', val)
}
</script>
<template>
  <div>
    <h2 class="mb10">Slider 单滑块滑动输入条基本使用({
   
   {  singleValue }})</h2>
    <Slider
      :min="0"
      :max="100"
      width="80%"
      v-model:value="singleValue"
      @change="onChange"/>
    <h2 class="mt30 mb10">Slider 双滑块滑动输入条基本使用(range & {
   
   { doubleValue }})</h2>
    <Slider
      :min="0"
      :max="100"
      width="80%"
      range
      v-model:value="doubleValue"
      @change="onChange"/>
    <h2 class="mt30 mb10">Slider 禁用双滑块滑动输入条(range & disabled & {
   
   { doubleValue }})</h2>
    <Slider
      :min="0"
      :max="100"
      width="80%"
      disabled
      range
      v-model:value="doubleValue"
      @change="onChange"/>
  </div>
</template>
<style lang="less" scoped>

</style>

猜你喜欢

转载自blog.csdn.net/Dandrose/article/details/129984853