123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 |
- <template>
- <view
- class="slider-range-body"
- :class="{
- disabled: disabled,
- padding: tips || scaleInfo.show
- }">
-
- <!-- 刻度线 -->
- <view class="scale-bar" v-if="scaleInfo.show">
- <view
- class="scale-item"
- v-for="(scaleItem, scaleIndex) in scaleLine" :key="scaleIndex"
- :style="{
- left: scaleItem.left,
- color: scaleItem.color,
- fontSize: scaleItem.fontSize,
- }">
- {{ scaleItem.label }}
- <view
- class="scale-item-line"
- :style="{
- backgroundColor: scaleItem.tickColor
- }"
- />
- </view>
- </view>
-
- <!-- 滑动提示 -->
- <view class="tips-bar" v-if="tips">
- <view
- class="tips-item"
- v-if="dragging == 'firstBlock'"
- :style="{
- left: getFirstBlockPosition,
- zIndex: firstBlock.zIndex
- }">
- <!-- #ifndef MP-WEIXIN -->
- <slot name="tips" :value="firstValue">
- <view class="default-tips">{{ firstValue }}</view>
- </slot>
- <!-- #endif -->
- <!-- #ifdef MP-WEIXIN -->
- <slot name="firstTips">
- <view class="default-tips">{{ firstValue }}</view>
- </slot>
- <!-- #endif -->
- </view>
- <view
- class="tips-item"
- v-if="dragging == 'secondBlock'"
- :style="{
- left: getSecondBlockPosition,
- zIndex: secondBlock.zIndex
- }">
- <!-- #ifndef MP-WEIXIN -->
- <slot name="tips" :value="secondValue">
- <view class="default-tips">{{ secondValue }}</view>
- </slot>
- <!-- #endif -->
- <!-- #ifdef MP-WEIXIN -->
- <slot name="secondTips">
- <view class="default-tips">{{ secondValue }}</view>
- </slot>
- <!-- #endif -->
- </view>
- </view>
-
- <!-- 滑动条 -->
- <view
- class="slider-bar"
- :style="[getBackgroundStyle]">
- <view
- class="slider-active-bar"
- :style="[getActiveStyle]"
- ></view>
- </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="default-block block-font"></view>
- </slot>
- <!-- #endif -->
- <!-- #ifdef MP-WEIXIN -->
- <slot name="firstBlock">
- <view class="default-block block-font"></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">
- <!-- #ifndef MP-WEIXIN -->
- <slot name="block">
- <view class="default-block block-font"></view>
- </slot>
- <!-- #endif -->
- <!-- #ifdef MP-WEIXIN -->
- <slot name="secondBlock">
- <view class="default-block block-font"></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>
- </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: 100
- },
- step: {
- type: Number,
- default: 1
- },
- 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: 0,
- secondValue: 0,
- // 两个滑点
- firstBlock: {
- show: true,
- zIndex: 1,
- },
- secondBlock: {
- show: true,
- zIndex: 2,
- },
- // 滑动中
- dragging: '',
- // 刻度线设置
- 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 + '%',
- }
- }
- },
- 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
- 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
- }
- // 计算刻度值
- if(typeof this.scale != 'object'){
- this.scaleInfo.show = !!this.scale
- }else{
- this.scaleInfo = {...this.scaleInfo, ...this.scale}
- }
- if(!this.scaleInfo.show) return
- let interval = this.scaleInfo.interval
- let step = this.getStep > 0 ? this.getStep : 1
- if(typeof interval != 'number'){
- interval = Math.ceil((this.max - this.min) / step / 10)
- }else{
- interval = interval + 1
- }
- let cumsum = this.min
- let arr = []
- while(cumsum <= this.max){
- arr.push({
- value: cumsum,
- label: cumsum,
- color: this.scaleInfo.color,
- fontSize: completeUnit(this.scaleInfo.fontSize),
- tickColor: this.scaleInfo.tickColor,
- left: (cumsum - this.min) / (this.max - this.min) * 100 + '%'
- })
- cumsum = cumsum + multiplication(interval, step)
- }
- if(arr[0].value == this.min && !this.scaleInfo.min) arr = arr.slice(1)
- if(arr[arr.length -1].value == this.max && !this.scaleInfo.max) arr = arr.slice(0, arr.length -1)
- if(arr[arr.length -1].value != this.max && this.scaleInfo.max) arr.push({
- value: this.max,
- label: this.max,
- color: this.scaleInfo.color,
- fontSize: completeUnit(this.scaleInfo.fontSize),
- tickColor: this.scaleInfo.tickColor,
- left: '100%'
- })
- let format = this.scaleInfo.format
- if(typeof format == 'function'){
- arr = arr.map((item, index) => {
- return format(item, index)
- })
- }
- this.scaleLine = [...arr]
- this.$forceUpdate()
- },
- onTouchStart(e){
- if(this.disabled) return
- this.dragging = e.currentTarget.dataset.sort
- 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].pageX : e.pageX
- 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)]
- })
- },
- onTouchMove(e){
- if(this.disabled) return
- this.onDrag(e)
- },
- onTouchEnd(e){
- this.dragging = ''
- if(this.disabled) return
- this.onDrag(e, true)
- },
- onDrag(e, end = false){
- let pageX = e.changedTouches ? e.changedTouches[0].pageX : e.pageX
- let view = uni.createSelectorQuery().in(this).select('.block-bar')
- view.boundingClientRect(data => {
- let diff = ((pageX - this.startDragPostion) / data.width) * (this.max - this.min)
- this.onUpdateValue(this.startValue + diff, e.currentTarget.dataset.sort, end)
- }).exec()
- },
- onUpdateValue(value, sort, end = false){
- 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
- }
- this.$emit('change', {
- firstValue: this.firstValue,
- secondValue: this.secondValue,
- values: [Math.min(this.firstValue, this.secondValue), Math.max(this.firstValue, this.secondValue)]
- })
- if(end){
- this.$emit('end', {
- block: sort,
- value: value,
- values: [Math.min(this.firstValue, this.secondValue), Math.max(this.firstValue, this.secondValue)]
- })
- }
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- @font-face {
- font-family: 'block-font';
- src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAK8AAsAAAAABnAAAAJvAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACCcApsgQgBNgIkAwgLBgAEIAWEZwcvG8MFyJ6aPCkSYgMLUMxWfTCChHj4WkPf39sgkItGUKiiTMrOVnmWnRpxV/v99mPaxBdpKnZP/N7cW6x5SV7/UAI0T6aVIZMZ2movuV6lLBp1KX/zBrwzj+/46Y16pUYVDyjxgOQB04slFMJMxEMeBfVaXwOQEc8I1FpWL+CwvXcUYt6WyIEkQezRIGuON11dgjCdlmVaqxYq0b1ZfFNBek+3wNfo8/FfcYSRVBTstpPXNgnOf+KdBYGb5SbFD5vz9dDHEipQYB7IxEV3+UigaJIC1YaDjNLOLPgX/18GIbFrv1B/nd1YD/qp4Z6MnulWawlua6JhYG1UGzHs5uiDw3/gSRZb18/XZ8qc05vx1z2uG3vcbsrWSqdLJiZKpkrvXDVUUzV/ljaCI8mt5X0x7WM8rmu/cb8rjlNrjKHW/BKOQCNAeVLsFYvgQ/Djd7pqIbb26roM/hrt71GyPMmARUH1fsPBT3I+gMRsZEn5WAY5nAyf11br5CjBjrf6Gutpu5l6qNY2IlqlL0Oh2iiVuVlUqLOKStUOUGtO2+E6bVyBItdg0g5BaOZH0ug7Cs0uqMw9o0Knb1Rq9odaVxF9Xp3JYC1CXiJZwuUN1Bl3FNdNkdZhWV2xlkRdYVwWnk5zWJRfWK51yI4sptjgrarFRAoqgtvYLh+TLYujK7ghM8rXiNzqggKl6S35jNvQQpAtITIJWrYB6RjOoQRkKn7fMJlqhWWJAKnxsuDRqXZUJF8hQHU4HGjgTl7xrFIVI0SBFAJnQ+16lsxi4ZDbPMsgY0g+bUTRVa3AISmoI399l/1px6CW7U3hTClUPFqcmQAAAA==') format('woff2');
- }
- .block-font {
- font-family: "block-font" !important;
- font-style: normal;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- &:before{
- content: "\e917";
- }
- }
- .slider-range-body{
- --main-color: #333333;
- --primary-color: #2979ff;
- width: 100%;
- box-sizing: border-box;
- position: relative;
- &.disabled{
- opacity: .7;
- }
- &.padding{
- padding-top: 72rpx;
- }
- .slider-bar{
- width: 100%;
- height: 12rpx;
- background-color: #f8f8f8;
- border-radius: 999999rpx;
- overflow: hidden;
- position: relative;
- .slider-active-bar{
- height: 100%;
- background-color: var(--primary-color);
- border-radius: 999999rpx;
- position: absolute;
- }
- }
- .block-bar{
- width: 100%;
- margin-top: 8rpx;
- position: relative;
- .block-item{
- position: absolute;
- top: 0;
- left: 0;
- transform: translateX(-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;
- }
- }
- }
- .tips-bar{
- width: 100%;
- height: 60rpx;
- position: absolute;
- top: 0;
- .tips-item{
- position: absolute;
- top: 0;
- left: 0;
- transform: translateX(-50%);
- .default-tips{
- width: 60rpx;
- height: 60rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- background-color: #ffffff;
- box-shadow: 0 0 20rpx rgba(0, 0, 0, .4);
- color: var(--main-color);
- font-size: 22rpx;
- font-weight: 500;
- }
- }
- }
- .scale-bar{
- width: 100%;
- height: 60rpx;
- position: absolute;
- top: 0;
- .scale-item{
- display: flex;
- flex-direction: column;
- align-items: center;
- color: #333333;
- font-size: 22rpx;
- position: absolute;
- bottom: 0;
- transform: translateX(-50%);
- .scale-item-line{
- width: 2rpx;
- height: 8rpx;
- background-color: #999999;
- }
- }
- }
- }
- </style>
|