123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- <template>
- <view
- class="u-slider"
- :class="[{ 'u-slider--vertical': vertical, 'u-slider--disabled': disabled }]"
- :id="sliderId"
- :style="[sliderStyle]"
- @tap="onRailTap"
- >
- <!-- 轨道 -->
- <view class="u-slider__rail" :style="[railStyle]">
- <view class="u-slider__track" :style="[trackStyle]"></view>
- </view>
- <!-- 单个滑块或起始滑块 -->
- <view
- v-if="!range || (Array.isArray(currentValue))"
- class="u-slider__thumb"
- :style="[startThumbStyle]"
- @touchstart.stop.prevent="onThumbStart($event,'start')"
- @touchmove.stop.prevent="onThumbMove"
- @touchend.stop.prevent="onThumbEnd"
- >
- <slot name="startThumb">
- <view class="u-slider__thumb-inner" :style="[thumbStyle]"></view>
- </slot>
- <view v-if="showValue" class="u-slider__value">{{ displayStartValue }}</view>
- </view>
- <!-- 范围滑块结束滑块 -->
- <view
- v-if="range && (Array.isArray(currentValue))"
- class="u-slider__thumb u-slider__thumb--end"
- :style="[endThumbStyle]"
- @touchstart.stop.prevent="onThumbStart($event,'end')"
- @touchmove.stop.prevent="onThumbMove"
- @touchend.stop.prevent="onThumbEnd"
- >
- <slot name="endThumb">
- <view class="u-slider__thumb-inner" :style="[thumbStyle]"></view>
- </slot>
- <view v-if="showValue" class="u-slider__value">{{ displayEndValue }}</view>
- </view>
- </view>
- </template>
- <script>
- import props from './props.js';
- import mixin from '../../libs/mixin/mixin'
- import mpMixin from '../../libs/mixin/mpMixin'
- /**
- * Slider 滑块
- * @description 本组件用于滑动选择数值
- * @tutorial https://uviewui.com/components/slider.html
- * @property {String | Number} value 当前值 (默认 0 )
- * @property {String | Number} min 最小值 (默认 0 )
- * @property {String | Number} max 最大值 (默认 100 )
- * @property {String | Number} step 步长 (默认 1 )
- * @property {Boolean} range 是否开启双滑块模式 (默认 false )
- * @property {Boolean} disabled 是否禁用滑块 (默认 false )
- * @property {Boolean} reverse 是否反向移动 (默认 false )
- * @property {Boolean} readonly 是否为只读状态 (默认 false )
- * @property {Boolean} noCross 是否禁止双滑块交叉 (默认 false )
- * @property {Boolean} vertical 是否垂直展示 (默认 false )
- * @property {String} size 滑块的尺寸 (默认 24 )
- * @property {String} thumbSize 滑块大小 (默认 15 )
- * @property {String} thumbColor 滑块颜色 (默认 '#ffffff' )
- * @property {String} thumbBorder 滑块边框颜色 (默认 '3px solid ' + theme.primary )
- * @property {String} thumbRadius 滑块圆角 (默认 50 )
- * @property {String} railColor 轨道颜色 (默认 'rgba(0, 0, 0, 0.1)' )
- * @property {String} railRadius 轨道圆角 (默认 10 )
- * @property {String} railSize 轨道大小 (默认 4 )
- * @property {String} trackColor 已选择部分的轨道颜色 (默认 theme.primary )
- * @property {Boolean} showValue 是否显示数值 (默认 false )
- * @example <u-slider></u-slider>
- */
- export default {
- name: 'u-slider',
- mixins: [mpMixin, mixin, props],
-
- data() {
- return {
- sliderId: 'slider' + uni.$u.guid(),
- containerRect: null,
- activeThumb: null,
- isDragging: false,
- }
- },
- computed: {
- currentValue() {
- let value = 0;
- // #ifdef VUE2
- value = this.value;
- // #endif
- // #ifdef VUE3
- value = this.modelValue;
- // #endif
- if (this.range) {
- const val = Array.isArray(value) ? value : [this.min, this.max];
- return [this.safeNumber(val[0], this.min), this.safeNumber(val[1], this.max)];
- }
- return this.safeNumber(value, this.min);
- },
- displayStartValue() {
- return Array.isArray(this.currentValue) ? this.currentValue[0] : this.currentValue;
- },
- displayEndValue() {
- return Array.isArray(this.currentValue) ? this.currentValue[1] : '';
- },
- // 数值范围计算
- numericRange() {
- const min = this.safeNumber(this.min, 0);
- const max = this.safeNumber(this.max, 100);
- return { min, max, range: max - min };
- },
- thumbStyle() {
- let style = {
- width: uni.$u.addUnit(this.thumbSize),
- height: uni.$u.addUnit(this.thumbSize),
- borderRadius: uni.$u.addUnit(this.thumbRadius),
- backgroundColor: this.thumbColor,
- border: this.thumbBorder,
- };
- return style;
- },
- sliderStyle(){
- let style = {};
- if(this.vertical) {
- style.width = uni.$u.addUnit(this.size);
- } else {
- style.height = uni.$u.addUnit(this.size);
- }
- return style;
- },
- railStyle() {
- let style = {
- backgroundColor: this.railColor,
- borderRadius: uni.$u.addUnit(this.railRadius)
- };
- if(this.vertical) {
- style.width = uni.$u.addUnit(this.railSize);
- } else {
- style.height = uni.$u.addUnit(this.railSize);
- }
- return style;
- },
- // 计算轨道样式
- trackStyle() {
- const { min, max } = this.numericRange;
- const rect = this.containerRect;
- if (!rect) return {};
- let startFraction = 0;
- let endFraction = 0;
-
- let style = {
- backgroundColor: this.trackColor,
- borderRadius: uni.$u.addUnit(this.railRadius)
- };
- if (this.range && Array.isArray(this.currentValue)) {
- const firstThumbFraction = this.positionPercent(this.currentValue[0], min, max, this.vertical, this.reverse);
- const secondThumbFraction = this.positionPercent(this.currentValue[1], min, max, this.vertical, this.reverse);
- startFraction = firstThumbFraction < secondThumbFraction ? firstThumbFraction : secondThumbFraction;
- endFraction = firstThumbFraction < secondThumbFraction ? secondThumbFraction : firstThumbFraction;
- } else {
- const currentFraction = this.positionPercent(this.currentValue, min, max, this.vertical, this.reverse);
- if (this.reverse) {
- startFraction = currentFraction;
- endFraction = 1;
- } else {
- startFraction = 0;
- endFraction = currentFraction;
- }
-
- if (!this.reverse){
- style.opacity = currentFraction <= 0.05 ? 0 : 1;
- }
- }
-
- if(this.vertical) {
- const start = parseInt(startFraction * rect.height);
- const size = parseInt((endFraction - startFraction) * rect.height);
- style.bottom = uni.$u.addUnit(start);
- style.height = uni.$u.addUnit(size);
- style.width = uni.$u.addUnit(this.railSize);
- } else {
- const start = parseInt(startFraction * rect.width);
- const size = parseInt((endFraction - startFraction) * rect.width);
- style.left = uni.$u.addUnit(start);
- style.width = uni.$u.addUnit(size);
- style.height = uni.$u.addUnit(this.railSize);
- }
- return style;
- },
- // 计算起始滑块样式
- startThumbStyle() {
- const value = this.range && Array.isArray(this.currentValue) ? this.currentValue[0] : this.currentValue;
- return this.calculateThumbStyle(value);
- },
- // 计算结束滑块样式
- endThumbStyle() {
- return this.calculateThumbStyle(this.currentValue[1]);
- }
- },
- mounted() {
- this.$nextTick(() => this.measure());
- },
-
- // #ifdef VUE3
- emits: ['update:modelValue', 'change', 'dragStart', 'dragEnd'],
- // #endif
- methods: {
- measure() {
- const query = uni.createSelectorQuery().in(this);
- query.select('#' + this.sliderId).boundingClientRect(rect => {
- if (rect && rect.width > 0 && rect.height > 0) {
- this.containerRect = rect;
- }
- }).exec();
- },
- safeNumber(value, defaultValue = 0) {
- const num = Number(value);
- return isNaN(num) || !isFinite(num) ? defaultValue : num;
- },
- // 计算百分比位置
- percentFromValue(value, min, max) {
- const val = this.safeNumber(value, min);
- const minVal = this.safeNumber(min, 0);
- const maxVal = this.safeNumber(max, 100);
- const range = maxVal - minVal;
-
- if (range <= 0) return 0;
- return Math.max(0, Math.min(1, (val - minVal) / range));
- },
-
- // 计算位置百分比
- positionPercent(value, min, max, vertical, reverse) {
- const p = this.percentFromValue(value, min, max);
- return reverse ? (1 - p) : p;
- },
-
- // 计算滑块样式
- calculateThumbStyle(value) {
- const { min, max } = this.numericRange;
- const rect = this.containerRect;
- if (!rect) return {};
- const fraction = this.positionPercent(value, min, max, this.vertical, this.reverse);
- const clampedFraction = Math.max(0, Math.min(1, fraction));
- const thumbSize = uni.$u.getPx(this.thumbSize) / 2;
- let style = {}
- if(this.vertical){
- let raw = clampedFraction * rect.height;
- if (!this.reverse){
- raw -= thumbSize;
- }
- const position = Math.max(thumbSize, raw);
- style.bottom = uni.$u.addUnit(parseInt(position));
- } else {
- let raw = clampedFraction * rect.width;
- if (!this.reverse){
- raw -= thumbSize;
- }
- const position = Math.max(thumbSize, raw);
- style.left = uni.$u.addUnit(parseInt(position));
- }
- return style;
- },
- coerceToStep(value) {
- const { min, max } = this.numericRange;
- const step = this.safeNumber(this.step, 1);
- const stepValue = step > 0 ? step : 1;
-
- let v = this.safeNumber(value, min);
- v = min + Math.round((v - min) / stepValue) * stepValue;
- return Math.max(min, Math.min(max, v));
- },
- pointToValue(event) {
- const point = event.touches[0];
-
- const { min, max, range } = this.numericRange;
- const rect = this.containerRect;
-
- let position = 0;
- if (this.vertical) {
- if (rect.height <= 0) return null;
- position = 1 - ((point.clientY - rect.top) / rect.height);
- } else {
- if (rect.width <= 0) return null;
- position = (point.clientX - rect.left) / rect.width;
- }
-
- position = Math.max(0, Math.min(1, position));
- if (this.reverse) position = 1 - position;
-
- return this.coerceToStep(min + position * range);
- },
- updateValue(proposedValue, movedThumb) {
- if (this.disabled || this.readonly) return;
-
- let nextValue = '';
- const { min, max } = this.numericRange;
- const steppedValue = this.coerceToStep(proposedValue);
-
- if (this.range && Array.isArray(this.currentValue)) {
- let [startValue, endValue] = this.currentValue;
-
- if (movedThumb === 'end') {
- endValue = steppedValue;
- if (this.noCross && endValue < startValue) endValue = startValue;
- } else {
- startValue = steppedValue;
- if (this.noCross && startValue > endValue) startValue = endValue;
- }
-
- // 确保范围有效
- startValue = Math.max(min, Math.min(max, startValue));
- endValue = Math.max(min, Math.min(max, endValue));
-
- nextValue = [startValue, endValue];
- } else {
- const clampedValue = Math.max(min, Math.min(max, steppedValue));
- nextValue = clampedValue;
- }
- // #ifdef VUE2
- this.$emit('input', nextValue);
- // #endif
- // #ifdef VUE3
- this.$emit('update:modelValue', nextValue);
- // #endif
- },
- onRailTap(e) {
- if (this.disabled || this.readonly) return;
- uni.$u.throttle(() => {
- const v = this.pointToValue(e);
- if (v == null) return;
-
- if (this.range && Array.isArray(this.currentValue)) {
- // 点击移动更近的滑块
- const distStart = Math.abs(v - this.currentValue[0]);
- const distEnd = Math.abs(v - this.currentValue[1]);
- const thumb = distStart <= distEnd ? 'start' : 'end';
- this.updateValue(v, thumb);
- } else {
- this.updateValue(v, 'start');
- }
-
- this.$emit('change', this.currentValue);
- }, 50);
- },
-
- onThumbStart(e, thumb) {
- if (this.disabled || this.readonly) return;
- this.isDragging = true;
- this.activeThumb = thumb;
- this.measure();
- this.$emit('dragStart', this.currentValue);
- },
-
- onThumbMove(e) {
- if (!this.isDragging) return;
- const v = this.pointToValue(e);
- if (v) {
- this.updateValue(v, this.activeThumb);
- }
- },
-
- onThumbEnd(e) {
- if (!this.isDragging) return;
- this.isDragging = false;
- this.$emit('dragEnd', this.currentValue);
- this.$emit('change', this.currentValue);
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- @import "../../libs/css/components.scss";
- .u-slider {
- position: relative;
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- &--disabled {
- opacity: 0.5;
- }
- &--vertical {
- height: 100%;
- justify-content: center;
- }
-
- &--vertical .u-slider__rail {
- left: 50%;
- top: 0;
- transform: translateX(-50%);
- height: 100%;
- width: 100%;
- }
- &--vertical .u-slider__thumb {
- transform: translate(0%, 50%);
- }
- &__rail {
- width: 100%;
- }
- &__thumb {
- position: absolute;
- z-index: 2;
- left: 0;
- transform: translate(-50%, 0%);
- }
- &__track {
- position: absolute;
- transition-duration: .1s;
- transition-property: opacity;
- }
- &__thumb-inner {
- box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
- }
-
- &__value {
- position: absolute;
- top: -24px;
- left: 50%;
- transform: translateX(-50%);
- padding: 2px 6px;
- background: rgba(0,0,0,0.6);
- color: #fff;
- font-size: 10px;
- border-radius: 4px;
- }
- }
- </style>
|