358 lines
11 KiB
Vue
358 lines
11 KiB
Vue
|
<template>
|
|||
|
<view :class="rootClass" :style="customStyle" :id="sliderId">
|
|||
|
<!-- #ifdef MP-DINGTALK -->
|
|||
|
<view :id="sliderId" style="flex: 1" :class="rootClass">
|
|||
|
<!-- #endif -->
|
|||
|
<view :class="`wd-slider__label-min ${customMinClass}`" v-if="!hideMinMax">
|
|||
|
{{ minValue }}
|
|||
|
</view>
|
|||
|
<view class="wd-slider__bar-wrapper" :style="barWrapperStyle">
|
|||
|
<view class="wd-slider__bar" :style="barCustomStyle"></view>
|
|||
|
<!-- 左边 -->
|
|||
|
<view
|
|||
|
class="wd-slider__button-wrapper"
|
|||
|
:style="buttonLeftStyle"
|
|||
|
@touchstart="onTouchStart"
|
|||
|
@touchmove="onTouchMove"
|
|||
|
@touchend="onTouchEnd"
|
|||
|
@touchcancel="onTouchEnd"
|
|||
|
>
|
|||
|
<view class="wd-slider__label" v-if="!hideLabel">{{ leftNewValue }}</view>
|
|||
|
<view class="wd-slider__button" />
|
|||
|
</view>
|
|||
|
<!-- 右边 -->
|
|||
|
<view
|
|||
|
v-if="showRight"
|
|||
|
class="wd-slider__button-wrapper"
|
|||
|
:style="buttonRightStyle"
|
|||
|
@touchstart="onTouchStartRight"
|
|||
|
@touchmove="onTouchMoveRight"
|
|||
|
@touchend="onTouchEndRight"
|
|||
|
@touchcancel="onTouchEndRight"
|
|||
|
>
|
|||
|
<view class="wd-slider__label" v-if="!hideLabel">{{ rightNewValue }}</view>
|
|||
|
<view class="wd-slider__button" />
|
|||
|
</view>
|
|||
|
</view>
|
|||
|
<view :class="`wd-slider__label-max ${customMaxClass}`" v-if="!hideMinMax">
|
|||
|
{{ maxValue }}
|
|||
|
</view>
|
|||
|
<!-- #ifdef MP-DINGTALK -->
|
|||
|
</view>
|
|||
|
<!-- #endif -->
|
|||
|
</view>
|
|||
|
</template>
|
|||
|
|
|||
|
<script lang="ts">
|
|||
|
export default {
|
|||
|
name: 'wd-slider',
|
|||
|
options: {
|
|||
|
addGlobalClass: true,
|
|||
|
virtualHost: true,
|
|||
|
styleIsolation: 'shared'
|
|||
|
}
|
|||
|
}
|
|||
|
</script>
|
|||
|
|
|||
|
<script lang="ts" setup>
|
|||
|
import { computed, getCurrentInstance, onMounted, ref } from 'vue'
|
|||
|
import { getRect, isArray, isDef, isNumber, uuid } from '../common/util'
|
|||
|
import { useTouch } from '../composables/useTouch'
|
|||
|
import { watch } from 'vue'
|
|||
|
import { sliderProps, type SliderExpose } from './types'
|
|||
|
|
|||
|
const props = defineProps(sliderProps)
|
|||
|
const emit = defineEmits(['dragstart', 'dragmove', 'dragend', 'update:modelValue'])
|
|||
|
|
|||
|
// 存放右滑轮中的所有属性
|
|||
|
const rightSlider = {
|
|||
|
startValue: 0,
|
|||
|
deltaX: 0,
|
|||
|
newValue: 0
|
|||
|
}
|
|||
|
const sliderId = ref<string>(`sliderId${uuid()}`)
|
|||
|
|
|||
|
const touchLeft = useTouch()
|
|||
|
const touchRight = useTouch()
|
|||
|
|
|||
|
const showRight = ref<boolean>(false)
|
|||
|
const barStyle = ref<string>('')
|
|||
|
const leftNewValue = ref<number>(0)
|
|||
|
const rightNewValue = ref<number>(0)
|
|||
|
const rightBarPercent = ref<number>(0)
|
|||
|
const leftBarPercent = ref<number>(0)
|
|||
|
const trackWidth = ref<number>(0)
|
|||
|
const trackLeft = ref<number>(0)
|
|||
|
const startValue = ref<number>(0)
|
|||
|
const currentValue = ref<number | number[]>(0)
|
|||
|
const newValue = ref<number>(0)
|
|||
|
|
|||
|
const maxValue = ref<number>(100) // 最大值
|
|||
|
const minValue = ref<number>(0) // 最小值
|
|||
|
const stepValue = ref<number>(1) // 步长
|
|||
|
|
|||
|
watch(
|
|||
|
() => props.max,
|
|||
|
(newValue) => {
|
|||
|
if (newValue <= props.min) {
|
|||
|
maxValue.value = props.min // 交换最大值和最小值
|
|||
|
minValue.value = newValue
|
|||
|
console.warn('[wot design] warning(wd-slider): max value must be greater than min value')
|
|||
|
} else {
|
|||
|
maxValue.value = newValue // 更新最大值
|
|||
|
}
|
|||
|
calcBarPercent()
|
|||
|
},
|
|||
|
{ immediate: true }
|
|||
|
)
|
|||
|
|
|||
|
watch(
|
|||
|
() => props.min,
|
|||
|
(newValue) => {
|
|||
|
if (newValue >= props.max) {
|
|||
|
minValue.value = props.max // 交换最小值和最大值
|
|||
|
maxValue.value = newValue
|
|||
|
console.warn('[wot design] warning(wd-slider): min value must be less than max value')
|
|||
|
} else {
|
|||
|
minValue.value = newValue // 更新最小值
|
|||
|
}
|
|||
|
calcBarPercent()
|
|||
|
},
|
|||
|
{ immediate: true }
|
|||
|
)
|
|||
|
|
|||
|
watch(
|
|||
|
() => props.step,
|
|||
|
(newValue) => {
|
|||
|
if (newValue < 1) {
|
|||
|
stepValue.value = 1
|
|||
|
console.warn('[wot design] warning(wd-slider): step must be greater than 0')
|
|||
|
} else {
|
|||
|
stepValue.value = Math.floor(newValue)
|
|||
|
}
|
|||
|
},
|
|||
|
{ immediate: true }
|
|||
|
)
|
|||
|
|
|||
|
watch(
|
|||
|
() => props.modelValue,
|
|||
|
(newValue, oldValue) => {
|
|||
|
// 类型校验,支持所有值(除null、undefined。undefined建议统一写成void (0)防止全局undefined被覆盖)
|
|||
|
if (newValue === null || newValue === undefined) {
|
|||
|
emit('update:modelValue', oldValue)
|
|||
|
console.warn('[wot design] warning(wd-slider): value can nott be null or undefined')
|
|||
|
} else if (isArray(newValue) && newValue.length !== 2) {
|
|||
|
console.warn('[wot design] warning(wd-slider): value must be dyadic array')
|
|||
|
} else if (!isNumber(newValue) && !isArray(newValue)) {
|
|||
|
emit('update:modelValue', oldValue)
|
|||
|
console.warn('[wot design] warning(wd-slider): value must be dyadic array Or Number')
|
|||
|
}
|
|||
|
currentValue.value = newValue
|
|||
|
// 动态传值后修改
|
|||
|
if (isArray(newValue)) {
|
|||
|
if (oldValue && isArray(oldValue) && equal(newValue, oldValue)) return
|
|||
|
showRight.value = true
|
|||
|
if (leftBarPercent.value <= rightBarPercent.value) {
|
|||
|
leftBarSlider(newValue[0])
|
|||
|
rightBarSlider(newValue[1])
|
|||
|
} else {
|
|||
|
leftBarSlider(newValue[1])
|
|||
|
rightBarSlider(newValue[0])
|
|||
|
}
|
|||
|
} else {
|
|||
|
if (newValue === oldValue) return
|
|||
|
leftBarSlider(newValue)
|
|||
|
}
|
|||
|
},
|
|||
|
{ deep: true, immediate: true }
|
|||
|
)
|
|||
|
|
|||
|
const { proxy } = getCurrentInstance() as any
|
|||
|
|
|||
|
const rootClass = computed(() => {
|
|||
|
const rootClass = `wd-slider ${!props.hideLabel ? 'wd-slider__has-label' : ''} ${props.disabled ? 'wd-slider--disabled' : ''} ${props.customClass}`
|
|||
|
return rootClass
|
|||
|
})
|
|||
|
|
|||
|
const barWrapperStyle = computed(() => {
|
|||
|
const barWrapperStyle = `${props.inactiveColor ? 'background:' + props.inactiveColor : ''}`
|
|||
|
return barWrapperStyle
|
|||
|
})
|
|||
|
|
|||
|
const barCustomStyle = computed(() => {
|
|||
|
const barWrapperStyle = `${barStyle.value};${props.activeColor ? 'background:' + props.activeColor : ''}`
|
|||
|
return barWrapperStyle
|
|||
|
})
|
|||
|
|
|||
|
const buttonLeftStyle = computed(() => {
|
|||
|
const buttonLeftStyle = `left: ${leftBarPercent.value}%; visibility: ${!props.disabled ? 'show' : 'hidden'};`
|
|||
|
return buttonLeftStyle
|
|||
|
})
|
|||
|
|
|||
|
const buttonRightStyle = computed(() => {
|
|||
|
const buttonRightStyle = `left: ${rightBarPercent.value}%; visibility: ${!props.disabled ? 'show' : 'hidden'}`
|
|||
|
return buttonRightStyle
|
|||
|
})
|
|||
|
|
|||
|
onMounted(() => {
|
|||
|
initSlider()
|
|||
|
})
|
|||
|
|
|||
|
/**
|
|||
|
* 初始化slider宽度
|
|||
|
*/
|
|||
|
function initSlider() {
|
|||
|
getRect(`#${sliderId.value}`, false, proxy).then((data) => {
|
|||
|
// trackWidth: 轨道全长
|
|||
|
trackWidth.value = Number(data.width)
|
|||
|
// trackLeft: 轨道距离左侧的距离
|
|||
|
trackLeft.value = Number(data.left)
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
function onTouchStart(event: any) {
|
|||
|
const { disabled, modelValue } = props
|
|||
|
if (disabled) return
|
|||
|
touchLeft.touchStart(event)
|
|||
|
startValue.value = !isArray(modelValue)
|
|||
|
? format(modelValue)
|
|||
|
: leftBarPercent.value < rightBarPercent.value
|
|||
|
? format(modelValue[0])
|
|||
|
: format(modelValue[1])
|
|||
|
emit('dragstart', {
|
|||
|
value: currentValue.value
|
|||
|
})
|
|||
|
}
|
|||
|
function onTouchMove(event: any) {
|
|||
|
const { disabled } = props
|
|||
|
if (disabled) return
|
|||
|
touchLeft.touchMove(event)
|
|||
|
// 移动间距 deltaX 就是向左(-)向右(+)
|
|||
|
const diff = (touchLeft.deltaX.value / trackWidth.value) * (maxValue.value - minValue.value)
|
|||
|
newValue.value = startValue.value + diff
|
|||
|
|
|||
|
// 左滑轮滑动控制
|
|||
|
leftBarSlider(newValue.value)
|
|||
|
emit('dragmove', {
|
|||
|
value: currentValue.value
|
|||
|
})
|
|||
|
}
|
|||
|
function onTouchEnd() {
|
|||
|
if (props.disabled) return
|
|||
|
emit('dragend', {
|
|||
|
value: currentValue.value
|
|||
|
})
|
|||
|
}
|
|||
|
// 右边滑轮滑动状态监听
|
|||
|
function onTouchStartRight(event: any) {
|
|||
|
if (props.disabled) return
|
|||
|
const { modelValue } = props
|
|||
|
// 右滑轮移动时数据绑定
|
|||
|
touchRight.touchStart(event)
|
|||
|
// 记录开始数据值
|
|||
|
rightSlider.startValue = leftBarPercent.value < rightBarPercent.value ? format((modelValue as number[])[1]) : format((modelValue as number[])[0])
|
|||
|
emit('dragstart', {
|
|||
|
value: currentValue.value
|
|||
|
})
|
|||
|
}
|
|||
|
function onTouchMoveRight(event: any) {
|
|||
|
if (props.disabled) return
|
|||
|
touchRight.touchMove(event)
|
|||
|
// 移动间距 deltaX 就是向左向右
|
|||
|
const diff = (touchRight.deltaX.value / trackWidth.value) * (maxValue.value - minValue.value)
|
|||
|
rightSlider.newValue = format(rightSlider.startValue + diff)
|
|||
|
// 右滑轮滑动控制
|
|||
|
rightBarSlider(rightSlider.newValue)
|
|||
|
emit('dragmove', {
|
|||
|
value: currentValue.value
|
|||
|
})
|
|||
|
}
|
|||
|
function onTouchEndRight() {
|
|||
|
if (props.disabled) return
|
|||
|
emit('dragend', {
|
|||
|
value: currentValue.value
|
|||
|
})
|
|||
|
}
|
|||
|
/**
|
|||
|
* 控制右侧滑轮滑动, value校验
|
|||
|
* @param {Number} value 当前滑轮绑定值
|
|||
|
*/
|
|||
|
function rightBarSlider(value: number) {
|
|||
|
value = format(value)
|
|||
|
rightNewValue.value = value
|
|||
|
emit('update:modelValue', [Math.min(leftNewValue.value, rightNewValue.value), Math.max(leftNewValue.value, rightNewValue.value)])
|
|||
|
rightBarPercent.value = formatPercent(value)
|
|||
|
styleControl()
|
|||
|
}
|
|||
|
/**
|
|||
|
* 控制左滑轮滑动,更新渲染数据,对 value 进行校验取整
|
|||
|
* @param {Number} value 当前滑轮绑定值
|
|||
|
*/
|
|||
|
function leftBarSlider(value: number) {
|
|||
|
value = format(value)
|
|||
|
|
|||
|
// 把 value 转换成百分比
|
|||
|
const percent = formatPercent(value)
|
|||
|
if (!showRight.value) {
|
|||
|
emit('update:modelValue', value)
|
|||
|
leftNewValue.value = value
|
|||
|
leftBarPercent.value = percent
|
|||
|
barStyle.value = `width: ${percent}%; `
|
|||
|
} else {
|
|||
|
leftNewValue.value = value
|
|||
|
leftBarPercent.value = percent
|
|||
|
emit('update:modelValue', [Math.min(leftNewValue.value, rightNewValue.value), Math.max(leftNewValue.value, rightNewValue.value)])
|
|||
|
styleControl()
|
|||
|
}
|
|||
|
}
|
|||
|
// 样式控制
|
|||
|
function styleControl() {
|
|||
|
// 左右滑轮距离左边最短为当前激活条所处位置
|
|||
|
const barLeft =
|
|||
|
leftBarPercent.value < rightBarPercent.value ? [leftBarPercent.value, rightBarPercent.value] : [rightBarPercent.value, leftBarPercent.value]
|
|||
|
// 通过左右滑轮的间距控制 激活条宽度 barLeft[1] - barLeft[0]
|
|||
|
const barStyleTemp = `width: ${barLeft[1] - barLeft[0]}%; left: ${barLeft[0]}%`
|
|||
|
currentValue.value =
|
|||
|
leftNewValue.value < rightNewValue.value ? [leftNewValue.value, rightNewValue.value] : [rightNewValue.value, leftNewValue.value]
|
|||
|
barStyle.value = barStyleTemp
|
|||
|
}
|
|||
|
|
|||
|
function equal(arr1: number[], arr2: number[]) {
|
|||
|
if (!isDef(arr1) || !isDef(arr2)) {
|
|||
|
return false
|
|||
|
}
|
|||
|
let i = 0
|
|||
|
arr1.forEach((item, index) => {
|
|||
|
item === arr2[index] && i++
|
|||
|
})
|
|||
|
return i === 2
|
|||
|
}
|
|||
|
function format(value: number) {
|
|||
|
const formatValue = Math.round(Math.max(minValue.value, Math.min(value, maxValue.value)) / stepValue.value) * stepValue.value
|
|||
|
return formatValue
|
|||
|
}
|
|||
|
|
|||
|
// 计算占比
|
|||
|
function formatPercent(value: number) {
|
|||
|
const percentage = ((value - minValue.value) / (maxValue.value - minValue.value)) * 100
|
|||
|
return percentage
|
|||
|
}
|
|||
|
// 计算滑块位置和进度长度
|
|||
|
function calcBarPercent() {
|
|||
|
const { modelValue } = props
|
|||
|
let value = !isArray(modelValue) ? format(modelValue) : leftBarPercent.value < rightBarPercent.value ? format(modelValue[0]) : format(modelValue[1])
|
|||
|
value = format(value)
|
|||
|
// 把 value 转换成百分比
|
|||
|
const percent = formatPercent(value)
|
|||
|
leftBarPercent.value = percent
|
|||
|
barStyle.value = `width: ${percent}%; `
|
|||
|
}
|
|||
|
|
|||
|
defineExpose<SliderExpose>({
|
|||
|
initSlider
|
|||
|
})
|
|||
|
</script>
|
|||
|
<style lang="scss" scoped>
|
|||
|
@import './index.scss';
|
|||
|
</style>
|