<template> 
 | 
    <view class="u-number-box"> 
 | 
        <view 
 | 
            class="u-number-box__slot" 
 | 
            @tap.stop="clickHandler('minus')" 
 | 
            @touchstart="onTouchStart('minus')" 
 | 
            @touchend.stop="clearTimeout" 
 | 
            v-if="showMinus && $slots.minus" 
 | 
        > 
 | 
            <slot name="minus" /> 
 | 
        </view> 
 | 
        <view 
 | 
            v-else-if="showMinus" 
 | 
            class="u-number-box__minus" 
 | 
            @tap.stop="clickHandler('minus')" 
 | 
            @touchstart="onTouchStart('minus')" 
 | 
            @touchend.stop="clearTimeout" 
 | 
            hover-class="u-number-box__minus--hover" 
 | 
            hover-stay-time="150" 
 | 
            :class="{ 'u-number-box__minus--disabled': isDisabled('minus') }" 
 | 
            :style="[buttonStyle('minus')]" 
 | 
        > 
 | 
            <u-icon 
 | 
                name="minus" 
 | 
                :color="isDisabled('minus') ? '#c8c9cc' : '#323233'" 
 | 
                size="15" 
 | 
                bold 
 | 
                :customStyle="iconStyle" 
 | 
            ></u-icon> 
 | 
        </view> 
 | 
  
 | 
        <slot name="input"> 
 | 
            <input 
 | 
                :disabled="disabledInput || disabled" 
 | 
                :cursor-spacing="getCursorSpacing" 
 | 
                :class="{ 'u-number-box__input--disabled': disabled || disabledInput }" 
 | 
                v-model="currentValue" 
 | 
                class="u-number-box__input" 
 | 
                @blur="onBlur" 
 | 
                @focus="onFocus" 
 | 
                @input="onInput" 
 | 
                type="number" 
 | 
                :style="[inputStyle]" 
 | 
            /> 
 | 
        </slot> 
 | 
        <view 
 | 
            class="u-number-box__slot" 
 | 
            @tap.stop="clickHandler('plus')" 
 | 
            @touchstart="onTouchStart('plus')" 
 | 
            @touchend.stop="clearTimeout" 
 | 
            v-if="showPlus && $slots.plus" 
 | 
        > 
 | 
            <slot name="plus" /> 
 | 
        </view> 
 | 
        <view 
 | 
            v-else-if="showPlus" 
 | 
            class="u-number-box__plus" 
 | 
            @tap.stop="clickHandler('plus')" 
 | 
            @touchstart="onTouchStart('plus')" 
 | 
            @touchend.stop="clearTimeout" 
 | 
            hover-class="u-number-box__plus--hover" 
 | 
            hover-stay-time="150" 
 | 
            :class="{ 'u-number-box__minus--disabled': isDisabled('plus') }" 
 | 
            :style="[buttonStyle('plus')]" 
 | 
        > 
 | 
            <u-icon 
 | 
                name="plus" 
 | 
                :color="isDisabled('plus') ? '#c8c9cc' : '#323233'" 
 | 
                size="15" 
 | 
                bold 
 | 
                :customStyle="iconStyle" 
 | 
            ></u-icon> 
 | 
        </view> 
 | 
    </view> 
 | 
</template> 
 | 
  
 | 
<script> 
 | 
    import props from './props.js'; 
 | 
    /** 
 | 
     * numberBox 步进器 
 | 
     * @description 该组件一般用于商城购物选择物品数量的场景。 
 | 
     * @tutorial https://uviewui.com/components/numberBox.html 
 | 
     * @property {String | Number}    name            步进器标识符,在change回调返回 
 | 
     * @property {String | Number}    value            用于双向绑定的值,初始化时设置设为默认min值(最小值)  (默认 0 ) 
 | 
     * @property {String | Number}    min                最小值 (默认 1 ) 
 | 
     * @property {String | Number}    max                最大值 (默认 Number.MAX_SAFE_INTEGER ) 
 | 
     * @property {String | Number}    step            加减的步长,可为小数 (默认 1 ) 
 | 
     * @property {Boolean}            integer            是否只允许输入整数 (默认 false ) 
 | 
     * @property {Boolean}            disabled        是否禁用,包括输入框,加减按钮 (默认 false ) 
 | 
     * @property {Boolean}            disabledInput    是否禁用输入框 (默认 false ) 
 | 
     * @property {Boolean}            asyncChange        是否开启异步变更,开启后需要手动控制输入值 (默认 false ) 
 | 
     * @property {String | Number}    inputWidth        输入框宽度,单位为px (默认 35 ) 
 | 
     * @property {Boolean}            showMinus        是否显示减少按钮 (默认 true ) 
 | 
     * @property {Boolean}            showPlus        是否显示增加按钮 (默认 true ) 
 | 
     * @property {String | Number}    decimalLength    显示的小数位数 
 | 
     * @property {Boolean}            longPress        是否开启长按加减手势 (默认 true ) 
 | 
     * @property {String}            color            输入框文字和加减按钮图标的颜色 (默认 '#323233' ) 
 | 
     * @property {String | Number}    buttonSize        按钮大小,宽高等于此值,单位px,输入框高度和此值保持一致 (默认 30 ) 
 | 
     * @property {String}            bgColor            输入框和按钮的背景颜色 (默认 '#EBECEE' ) 
 | 
     * @property {String | Number}    cursorSpacing    指定光标于键盘的距离,避免键盘遮挡输入框,单位px (默认 100 ) 
 | 
     * @property {Boolean}            disablePlus        是否禁用增加按钮 (默认 false ) 
 | 
     * @property {Boolean}            disableMinus    是否禁用减少按钮 (默认 false ) 
 | 
     * @property {Object | String}    iconStyle        加减按钮图标的样式 
 | 
     * 
 | 
     * @event {Function}    onFocus    输入框活动焦点 
 | 
     * @event {Function}    onBlur    输入框失去焦点 
 | 
     * @event {Function}    onInput    输入框值发生变化 
 | 
     * @event {Function}    onChange 
 | 
     * @example <u-number-box v-model="value" @change="valChange"></u-number-box> 
 | 
     */ 
 | 
    export default { 
 | 
        name: 'u-number-box', 
 | 
        mixins: [uni.$u.mpMixin, uni.$u.mixin, props], 
 | 
        data() { 
 | 
            return { 
 | 
                // 输入框实际操作的值 
 | 
                currentValue: '', 
 | 
                // 定时器 
 | 
                longPressTimer: null 
 | 
            } 
 | 
        }, 
 | 
        watch: { 
 | 
            // 多个值之间,只要一个值发生变化,都要重新检查check()函数 
 | 
            watchChange(n) { 
 | 
                this.check() 
 | 
            }, 
 | 
            // 监听v-mode的变化,重新初始化内部的值 
 | 
            value(n) { 
 | 
                if (n !== this.currentValue) { 
 | 
                    this.currentValue = this.format(this.value) 
 | 
                } 
 | 
            } 
 | 
        }, 
 | 
        computed: { 
 | 
            getCursorSpacing() { 
 | 
                // 判断传入的单位,如果为px单位,需要转成px 
 | 
                return uni.$u.getPx(this.cursorSpacing) 
 | 
            }, 
 | 
            // 按钮的样式 
 | 
            buttonStyle() { 
 | 
                return (type) => { 
 | 
                    const style = { 
 | 
                        backgroundColor: this.bgColor, 
 | 
                        height: uni.$u.addUnit(this.buttonSize), 
 | 
                        color: this.color 
 | 
                    } 
 | 
                    if (this.isDisabled(type)) { 
 | 
                        style.backgroundColor = '#f7f8fa' 
 | 
                    } 
 | 
                    return style 
 | 
                } 
 | 
            }, 
 | 
            // 输入框的样式 
 | 
            inputStyle() { 
 | 
                const disabled = this.disabled || this.disabledInput 
 | 
                const style = { 
 | 
                    color: this.color, 
 | 
                    backgroundColor: this.bgColor, 
 | 
                    height: uni.$u.addUnit(this.buttonSize), 
 | 
                    width: uni.$u.addUnit(this.inputWidth) 
 | 
                } 
 | 
                return style 
 | 
            }, 
 | 
            // 用于监听多个值发生变化 
 | 
            watchChange() { 
 | 
                return [this.integer, this.decimalLength, this.min, this.max] 
 | 
            }, 
 | 
            isDisabled() { 
 | 
                return (type) => { 
 | 
                    if (type === 'plus') { 
 | 
                        // 在点击增加按钮情况下,判断整体的disabled,是否单独禁用增加按钮,以及当前值是否大于最大的允许值 
 | 
                        return ( 
 | 
                            this.disabled || 
 | 
                            this.disablePlus || 
 | 
                            this.currentValue >= this.max 
 | 
                        ) 
 | 
                    } 
 | 
                    // 点击减少按钮同理 
 | 
                    return ( 
 | 
                        this.disabled || 
 | 
                        this.disableMinus || 
 | 
                        this.currentValue <= this.min 
 | 
                    ) 
 | 
                } 
 | 
            }, 
 | 
        }, 
 | 
        mounted() { 
 | 
            this.init() 
 | 
        }, 
 | 
        methods: { 
 | 
            init() { 
 | 
                this.currentValue = this.format(this.value) 
 | 
            }, 
 | 
            // 格式化整理数据,限制范围 
 | 
            format(value) { 
 | 
                value = this.filter(value) 
 | 
                // 如果为空字符串,那么设置为0,同时将值转为Number类型 
 | 
                value = value === '' ? 0 : +value 
 | 
                // 对比最大最小值,取在min和max之间的值 
 | 
                value = Math.max(Math.min(this.max, value), this.min) 
 | 
                // 如果设定了最大的小数位数,使用toFixed去进行格式化 
 | 
                if (this.decimalLength !== null) { 
 | 
                    value = value.toFixed(this.decimalLength) 
 | 
                } 
 | 
                return value 
 | 
            }, 
 | 
            // 过滤非法的字符 
 | 
            filter(value) { 
 | 
                // 只允许0-9之间的数字,"."为小数点,"-"为负数时候使用 
 | 
                value = String(value).replace(/[^0-9.-]/g, '') 
 | 
                // 如果只允许输入整数,则过滤掉小数点后的部分 
 | 
                if (this.integer && value.indexOf('.') !== -1) { 
 | 
                    value = value.split('.')[0] 
 | 
                } 
 | 
                return value; 
 | 
            }, 
 | 
            check() { 
 | 
                // 格式化了之后,如果前后的值不相等,那么设置为格式化后的值 
 | 
                const val = this.format(this.currentValue); 
 | 
                if (val !== this.currentValue) { 
 | 
                    this.currentValue = val 
 | 
                } 
 | 
            }, 
 | 
            // 判断是否出于禁止操作状态 
 | 
            // isDisabled(type) { 
 | 
            //     if (type === 'plus') { 
 | 
            //         // 在点击增加按钮情况下,判断整体的disabled,是否单独禁用增加按钮,以及当前值是否大于最大的允许值 
 | 
            //         return ( 
 | 
            //             this.disabled || 
 | 
            //             this.disablePlus || 
 | 
            //             this.currentValue >= this.max 
 | 
            //         ) 
 | 
            //     } 
 | 
            //     // 点击减少按钮同理 
 | 
            //     return ( 
 | 
            //         this.disabled || 
 | 
            //         this.disableMinus || 
 | 
            //         this.currentValue <= this.min 
 | 
            //     ) 
 | 
            // }, 
 | 
            // 输入框活动焦点 
 | 
            onFocus(event) { 
 | 
                this.$emit('focus', { 
 | 
                    ...event.detail, 
 | 
                    name: this.name, 
 | 
                }) 
 | 
            }, 
 | 
            // 输入框失去焦点 
 | 
            onBlur(event) { 
 | 
                // 对输入值进行格式化 
 | 
                const value = this.format(event.detail.value) 
 | 
                // 发出blur事件 
 | 
                this.$emit( 
 | 
                    'blur',{ 
 | 
                        ...event.detail, 
 | 
                        name: this.name, 
 | 
                    } 
 | 
                ) 
 | 
            }, 
 | 
            // 输入框值发生变化 
 | 
            onInput(e) { 
 | 
                const { 
 | 
                    value = '' 
 | 
                } = e.detail || {} 
 | 
                // 为空返回 
 | 
                if (value === '') return 
 | 
                let formatted = this.filter(value) 
 | 
                // 最大允许的小数长度 
 | 
                if (this.decimalLength !== null && formatted.indexOf('.') !== -1) { 
 | 
                    const pair = formatted.split('.'); 
 | 
                    formatted = `${pair[0]}.${pair[1].slice(0, this.decimalLength)}` 
 | 
                } 
 | 
                formatted = this.format(formatted) 
 | 
                this.emitChange(formatted); 
 | 
            }, 
 | 
            // 发出change事件 
 | 
            emitChange(value) { 
 | 
                // 如果开启了异步变更值,则不修改内部的值,需要用户手动在外部通过v-model变更 
 | 
                if (!this.asyncChange) { 
 | 
                    this.$nextTick(() => { 
 | 
                        this.$emit('input', value) 
 | 
                        this.currentValue = value 
 | 
                        this.$forceUpdate() 
 | 
                    }) 
 | 
                } 
 | 
                this.$emit('change', { 
 | 
                    value, 
 | 
                    name: this.name, 
 | 
                }); 
 | 
            }, 
 | 
            onChange() { 
 | 
                const { 
 | 
                    type 
 | 
                } = this 
 | 
                if (this.isDisabled(type)) { 
 | 
                    return this.$emit('overlimit', type) 
 | 
                } 
 | 
                const diff = type === 'minus' ? -this.step : +this.step 
 | 
                const value = this.format(this.add(+this.currentValue, diff)) 
 | 
                this.emitChange(value) 
 | 
                this.$emit(type) 
 | 
            }, 
 | 
            // 对值扩大后进行四舍五入,再除以扩大因子,避免出现浮点数操作的精度问题 
 | 
            add(num1, num2) { 
 | 
                const cardinal = Math.pow(10, 10); 
 | 
                return Math.round((num1 + num2) * cardinal) / cardinal 
 | 
            }, 
 | 
            // 点击加减按钮 
 | 
            clickHandler(type) { 
 | 
                this.type = type 
 | 
                this.onChange() 
 | 
            }, 
 | 
            longPressStep() { 
 | 
                // 每隔一段时间,重新调用longPressStep方法,实现长按加减 
 | 
                this.clearTimeout() 
 | 
                this.longPressTimer = setTimeout(() => { 
 | 
                    this.onChange() 
 | 
                    this.longPressStep() 
 | 
                }, 250); 
 | 
            }, 
 | 
            onTouchStart(type) { 
 | 
                if (!this.longPress) return 
 | 
                this.clearTimeout() 
 | 
                this.type = type 
 | 
                // 一定时间后,默认达到长按状态 
 | 
                this.longPressTimer = setTimeout(() => { 
 | 
                    this.onChange() 
 | 
                    this.longPressStep() 
 | 
                }, 600) 
 | 
            }, 
 | 
            // 触摸结束,清除定时器,停止长按加减 
 | 
            onTouchEnd() { 
 | 
                if (!this.longPress) return 
 | 
                this.clearTimeout() 
 | 
            }, 
 | 
            // 清除定时器 
 | 
            clearTimeout() { 
 | 
                clearTimeout(this.longPressTimer) 
 | 
                this.longPressTimer = null 
 | 
            } 
 | 
        } 
 | 
    } 
 | 
</script> 
 | 
  
 | 
<style lang="scss" scoped> 
 | 
    @import '../../libs/css/components.scss'; 
 | 
  
 | 
    $u-numberBox-hover-bgColor: #E6E6E6 !default; 
 | 
    $u-numberBox-disabled-color: #c8c9cc !default; 
 | 
    $u-numberBox-disabled-bgColor: #f7f8fa !default; 
 | 
    $u-numberBox-plus-radius: 4px !default; 
 | 
    $u-numberBox-minus-radius: 4px !default; 
 | 
    $u-numberBox-input-text-align: center !default; 
 | 
    $u-numberBox-input-font-size: 15px !default; 
 | 
    $u-numberBox-input-padding: 0 !default; 
 | 
    $u-numberBox-input-margin: 0 2px !default; 
 | 
    $u-numberBox-input-disabled-color: #c8c9cc !default; 
 | 
    $u-numberBox-input-disabled-bgColor: #f2f3f5 !default; 
 | 
  
 | 
    .u-number-box { 
 | 
        @include flex(row); 
 | 
        align-items: center; 
 | 
  
 | 
        &__slot { 
 | 
            /* #ifndef APP-NVUE */ 
 | 
            touch-action: none; 
 | 
            /* #endif */ 
 | 
        } 
 | 
  
 | 
        &__plus, 
 | 
        &__minus { 
 | 
            width: 35px; 
 | 
            @include flex; 
 | 
            justify-content: center; 
 | 
            align-items: center; 
 | 
            /* #ifndef APP-NVUE */ 
 | 
            touch-action: none; 
 | 
            /* #endif */ 
 | 
  
 | 
            &--hover { 
 | 
                background-color: $u-numberBox-hover-bgColor !important; 
 | 
            } 
 | 
  
 | 
            &--disabled { 
 | 
                color: $u-numberBox-disabled-color; 
 | 
                background-color: $u-numberBox-disabled-bgColor; 
 | 
            } 
 | 
        } 
 | 
  
 | 
        &__plus { 
 | 
            border-top-right-radius: $u-numberBox-plus-radius; 
 | 
            border-bottom-right-radius: $u-numberBox-plus-radius; 
 | 
        } 
 | 
  
 | 
        &__minus { 
 | 
            border-top-left-radius: $u-numberBox-minus-radius; 
 | 
            border-bottom-left-radius: $u-numberBox-minus-radius; 
 | 
        } 
 | 
  
 | 
        &__input { 
 | 
            position: relative; 
 | 
            text-align: $u-numberBox-input-text-align; 
 | 
            font-size: $u-numberBox-input-font-size; 
 | 
            padding: $u-numberBox-input-padding; 
 | 
            margin: $u-numberBox-input-margin; 
 | 
            @include flex; 
 | 
            align-items: center; 
 | 
            justify-content: center; 
 | 
  
 | 
            &--disabled { 
 | 
                color: $u-numberBox-input-disabled-color; 
 | 
                background-color: $u-numberBox-input-disabled-bgColor; 
 | 
            } 
 | 
        } 
 | 
    } 
 | 
</style> 
 |