| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- <template>
- <!-- 区间滑动选择器容器 -->
- <view class="slider-range-body" :class="{
- disabled: disabled, // 禁用状态
- padding: tips || scaleInfo.show // 显示提示或刻度时添加 padding
- }">
- <!-- 滑动条 -->
- <view class="slider-bar" :style="[getBackgroundStyle]">
- <!-- 选中区域 -->
- <view class="slider-active-bar" :style="[getActiveStyle]"></view>
- <!-- 滑块容器 -->
- <view class="block-bar">
- <!-- 第一个滑块 -->
- <view class="block-item" v-if="firstBlock.show" :style="{
- left: getFirstBlockPosition,
- zIndex: firstBlock.zIndex
- }" data-sort="firstBlock" @touchstart.stop.prevent="onTouchStart" @touchmove.stop.prevent="onTouchMove"
- @touchend.stop.prevent="onTouchEnd">
- <!-- 非微信小程序插槽 -->
- <!-- #ifndef MP-WEIXIN -->
- <slot name="block">
- <view class="slider dis a-c j-c ">{{firstValue}}</view>
- </slot>
- <!-- #endif -->
- <!-- 微信小程序插槽 -->
- <!-- #ifdef MP-WEIXIN -->
- <slot name="firstBlock">
- <view class="slider dis a-c j-c ">{{firstValue}}</view>
- </slot>
- <!-- #endif -->
- </view>
- <!-- 第二个滑块 -->
- <view class="block-item" v-if="secondBlock.show" :style="{
- left: getSecondBlockPosition,
- zIndex: secondBlock.zIndex
- }" data-sort="secondBlock" @touchstart.stop.prevent="onTouchStart" @touchmove.stop.prevent="onTouchMove"
- @touchend.stop.prevent="onTouchEnd" @mousedown.stop="onTouchStart" @mousemove.stop="onTouchMove"
- @mouseleave.stop="onTouchEnd" @mouseup.stop="onTouchEnd">
- <!-- 非微信小程序插槽 -->
- <!-- #ifndef MP-WEIXIN -->
- <slot name="block">
- <view class="slider dis a-c j-c ">{{secondValue}}</view>
- </slot>
- <!-- #endif -->
- <!-- 微信小程序插槽 -->
- <!-- #ifdef MP-WEIXIN -->
- <slot name="secondBlock">
- <view class="slider dis a-c j-c ">{{secondValue}}</view>
- </slot>
- <!-- #endif -->
- </view>
- <!-- 占位元素,用于撑开容器高度 -->
- <view class="block-placeholder">
- <!-- 非微信小程序插槽 -->
- <!-- #ifndef MP-WEIXIN -->
- <slot name="block">
- <view class="default-block block-font"></view>
- </slot>
- <!-- #endif -->
- <!-- 微信小程序插槽 -->
- <!-- #ifdef MP-WEIXIN -->
- <slot name="placeholderBlock">
- <view class="default-block block-font"></view>
- </slot>
- <!-- #endif -->
- </view>
- </view>
- </view>
- <!-- 刻度条 -->
- <view class="scale-bar dis a-c j-s ">
- <text>¥{{min}}</text>
- <text>¥{{max}}</text>
- </view>
- </view>
- </template>
- <script>
- /**
- * @description 区间滑动选择组件
- * @example <slider-range></slider-range>
- * @property {number|array<number>} value 初始值,为 number 时表示滑动选择器,为 array<number> 时表示区间滑动选择器
- * @property {string} valueType 当 value 为 number 时,可滑动的是哪个值,可选值:min、max,默认 max
- * @property {number} min 最小值(最左侧值)默认 0
- * @property {number} max 最大值(最右侧值)默认 100
- * @property {number} step 步长,为 0 时不设步长,默认为 1
- * @property {number|string} height 滑动条高度,可自定义单位,默认单位 rpx
- * @property {boolean} disabled 是否禁用,默认 false
- * @property {boolean} tips 是否显示滑动提示,默认为 true
- * @property {boolean|object} scale 刻度条配置
- * @property {object} backgroundStyle 背景条自定义样式
- * @property {object} activeStyle 选中条自定义样式
- **/
- // 补全单位
- const completeUnit = (val = 0, unit = 'rpx') => {
- if (isNaN(Number(val))) return val
- return val + unit
- }
- // 乘法,防止小数计算失真
- const multiplication = (val1, val2) => {
- let dotIndex = String(val2).indexOf('.')
- if (dotIndex != -1) {
- let floatLength = String(val2).length - (dotIndex + 1)
- return val1 * (val2 * Math.pow(10, floatLength)) / Math.pow(10, floatLength)
- } else {
- return val1 * val2
- }
- }
- export default {
- name: "slider-range",
- props: {
- value: {
- type: [Number, Array],
- default: () => []
- },
- valueType: {
- type: String,
- default: 'max'
- },
- min: {
- type: Number,
- default: 0
- },
- max: {
- type: Number,
- default: 1000
- },
- step: {
- type: Number,
- default: 5
- },
- height: {
- type: [Number, String],
- default: 12
- },
- disabled: {
- type: Boolean,
- default: false
- },
- tips: {
- type: Boolean,
- default: true
- },
- scale: {
- type: [Boolean, Object],
- default: true
- },
- backgroundStyle: {
- type: Object,
- default: () => {
- return {
- width: '100%'
- }
- }
- },
- activeStyle: {
- type: Object,
- default: () => {
- return {}
- }
- }
- },
- data() {
- return {
- // 两个值
- firstValue: 200,
- secondValue: 0,
- // 两个滑点
- firstBlock: {
- show: true,
- zIndex: 1,
- },
- secondBlock: {
- show: true,
- zIndex: 2,
- },
- // 拖动中
- dragging: '',
- // H5 端拖动锁,防止 mouseup/mouseleave 重复触发 end,以及松手后再次移入时还在滑动
- isDragging: false,
- // 刻度线设置
- scaleInfo: {
- show: true,
- min: true,
- max: true,
- interval: 'auto',
- color: '#333',
- tickColor: '#999',
- fontSize: 22,
- format: null,
- },
- scaleLine: [],
- }
- },
- computed: {
- getStep() {
- if (this.max - this.min < this.step) return this.max - this.min
- return this.step
- },
- // 滑点坐标
- getFirstBlockPosition() {
- return (this.firstValue - this.min) / (this.max - this.min) * 100 + '%'
- },
- getSecondBlockPosition() {
- return (this.secondValue - this.min) / (this.max - this.min) * 100 + '%'
- },
- // 背景条样式
- getBackgroundStyle() {
- return {
- ...this.backgroundStyle,
- height: completeUnit(this.height)
- }
- },
- // 选中条样式
- getActiveStyle() {
- let min = Math.min(this.firstValue, this.secondValue)
- let max = Math.max(this.firstValue, this.secondValue)
- return {
- ...this.activeStyle,
- left: (min - this.min) / (this.max - this.min) * 100 + '%',
- width: (max - min) / (this.max - this.min) * 100 + '%',
- }
- }
- },
- watch: {
- /**
- * 监听value属性变化
- * @param {Array|Number} newValue 新值
- */
- value: {
- handler(newValue) {
- // 当value变化时重新初始化
- this.create()
- },
- deep: true
- }
- },
- mounted() {
- this.create()
- },
- methods: {
- /**
- * 初始化组件
- * 设置初始值和刻度信息
- */
- create() {
- // 验证参数有效性
- if (this.max <= this.min) {
- throw '[slider-range] max 属性不应小于或等于 min 属性'
- }
- // 设置初始值
- if (typeof this.value == 'number') {
- let value = this.value
- // 限制值在范围内
- if (this.value > this.max) value = this.max
- if (this.value < this.min) value = this.min
- // 根据valueType设置单滑块模式
- if (this.valueType == 'max') {
- this.firstBlock.show = false
- this.firstValue = this.min
- this.secondValue = value
- } else {
- this.secondBlock.show = false
- this.secondValue = this.max
- this.firstValue = value
- }
- } else {
- // 双滑块模式
- let firstValue = this.value[0] || this.min
- if (firstValue > this.max) firstValue = this.max
- if (firstValue < this.min) firstValue = this.min
- let secondValue = this.value[1] || this.max
- if (secondValue > this.max) secondValue = this.max
- if (secondValue < this.min) secondValue = this.min
- this.firstValue = firstValue
- this.secondValue = secondValue
- this.firstBlock.show = true
- this.secondBlock.show = true
- }
- },
- /**
- * 触摸开始事件
- * @param {Event} e 触摸事件对象
- */
- onTouchStart(e) {
- // 如果上一次拖动未结束,不响应新的 mousedown(避免 H5 端 mouseup 之后又 mousedown 混乱)
- if (this.isDragging) return
- if (this.disabled) return
- this.isDragging = true
- // 设置当前拖动的滑块
- this.dragging = e.currentTarget.dataset.sort
- // 调整z-index,确保当前拖动的滑块在上方
- e.currentTarget.dataset.sort == 'firstBlock' ?
- this.firstBlock.zIndex = this.secondBlock.zIndex + 1 :
- this.secondBlock.zIndex = this.firstBlock.zIndex + 1
- // 记录开始位置和初始值
- this.startDragPostion = (e.changedTouches && e.changedTouches[0]) ? e.changedTouches[0].pageX : (e.pageX || 0)
- this.startValue = e.currentTarget.dataset.sort == 'firstBlock' ? this.firstValue : this.secondValue
- // 触发开始事件
- this.$emit('start', {
- block: e.currentTarget.dataset.sort,
- value: this.startValue,
- values: [Math.min(this.firstValue, this.secondValue), Math.max(this.firstValue, this
- .secondValue)]
- })
- },
- /**
- * 触摸移动事件
- * @param {Event} e 触摸事件对象
- */
- onTouchMove(e) {
- if (this.disabled) return
- // 拖动已结束(mouseup/mouseleave 后又移入),直接拦截
- if (!this.isDragging) return
- this.onDrag(e)
- },
- /**
- * 触摸结束事件
- * @param {Event} e 触摸事件对象
- */
- onTouchEnd(e) {
- // 拖动已结束过(mouseup 和 mouseleave 都会触发),避免重复发 end
- if (!this.isDragging) return
- this.isDragging = false
- this.dragging = ''
- if (this.disabled) return
- this.onDrag(e, true)
- },
- /**
- * 拖动处理
- * @param {Event} e 触摸事件对象
- * @param {boolean} end 是否结束拖动
- */
- onDrag(e, end = false) {
- // 兼容 MouseEvent、TouchEvent、以及 e 为 null 的情况
- let pageX = 0
- if (e && e.changedTouches && e.changedTouches[0]) {
- pageX = e.changedTouches[0].pageX
- } else if (e && typeof e.pageX === 'number') {
- pageX = e.pageX
- }
- // 如果 end 时无法拿到 pageX,使用最后记录的 startDragPostion(也就是不移动,只确认 end)
- if (end && pageX === 0 && this.startDragPostion) {
- pageX = this.startDragPostion
- }
- // 兜底 e.currentTarget,取当前正在拖动的滑块
- let sort = (e && e.currentTarget && e.currentTarget.dataset && e.currentTarget.dataset.sort) || this.dragging
- let view = uni.createSelectorQuery().in(this).select('.block-bar')
- view.boundingClientRect(data => {
- // DOM 还没渲染好时 data 可能为 null,做兜底
- if (!data || !data.width) {
- if (end) {
- this.$emit('end', {
- block: sort,
- value: this.startValue,
- values: [Math.min(this.firstValue, this.secondValue), Math.max(this.firstValue, this.secondValue)]
- })
- }
- return
- }
- // 计算拖动距离对应的数值变化
- let diff = ((pageX - this.startDragPostion) / data.width) * (this.max - this.min)
- // 如果 diff 是 NaN,跳过这次更新
- if (isNaN(diff)) {
- if (end) {
- this.$emit('end', {
- block: sort,
- value: this.startValue,
- values: [Math.min(this.firstValue, this.secondValue), Math.max(this.firstValue, this.secondValue)]
- })
- }
- return
- }
- // 更新值
- this.onUpdateValue(this.startValue + diff, sort, end)
- }).exec()
- },
- /**
- * 更新滑块值
- * @param {number} value 新值
- * @param {string} sort 滑块标识
- * @param {boolean} end 是否结束拖动
- */
- onUpdateValue(value, sort, end = false) {
- // 入口 NaN 防护:如果 value 是 NaN,降级为当前滑块已有值
- if (isNaN(value)) {
- value = sort == 'firstBlock' ? this.firstValue : this.secondValue
- if (isNaN(value)) value = this.min
- }
- // 限制值在范围内
- if (value < this.min) value = this.min
- if (value > this.max) value = this.max
- // 应用步长
- if (this.getStep > 0 && value != this.min && value != this.max) {
- value = this.min + multiplication(Math.round((value - this.min) / this.getStep), this.getStep)
- } else {
- value = Number(value.toFixed(2))
- }
- // 更新对应滑块的值
- if (sort == 'firstBlock') {
- this.firstValue = value
- } else {
- this.secondValue = value
- }
- // 计算最终的范围值(确保从小到大排序)
- const minValue = Math.min(this.firstValue, this.secondValue)
- const maxValue = Math.max(this.firstValue, this.secondValue)
- // 触发change事件,返回排序后的范围值
- this.$emit('change', [minValue, maxValue])
- // 触发end事件,返回排序后的范围值
- if (end) {
- this.$emit('end', {
- block: sort,
- value: value,
- values: [minValue, maxValue]
- })
- }
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .slider-range-body {
- --main-color: #333333;
- --primary-color: #FDE935;
- width: 100%;
- box-sizing: border-box;
- position: relative;
- &.disabled {
- opacity: .7;
- }
- &.padding {
- margin-top: 30rpx;
- }
- .slider-bar {
- width: 100%;
- height: 12rpx;
- background-color: #f8f8f8;
- border-radius: 999999rpx;
- position: relative;
- .slider-active-bar {
- height: 100%;
- background-color: var(--primary-color);
- border-radius: 999999rpx;
- position: absolute;
- }
- .block-bar {
- width: 100%;
- .block-item {
- position: absolute;
- top: 50%;
- left: 0;
- transform: translate(-50%, -50%);
- }
- .block-placeholder {
- opacity: 0 !important;
- user-select: none;
- pointer-events: none;
- }
- .block-item,
- .block-placeholder {
- .default-block {
- color: var(--primary-color);
- font-size: 42rpx;
- }
- .slider {
- padding: 0 20rpx;
- box-sizing: border-box;
- background: #FFFBD7;
- border-radius: 20rpx 20rpx 20rpx 20rpx;
- border: 1rpx solid #FDE935;
- font-size: 28rpx;
- color: #333;
- }
- }
- }
- }
- .scale-bar {
- width: 100%;
- font-size: 24rpx;
- color: #666;
- margin-top: 14rpx;
- }
- }
- </style>
|