|
@@ -0,0 +1,291 @@
|
|
|
+<template>
|
|
|
+ <!-- 参考https://github.com/youzan/vant/blob/2.x/src/stepper/index.js -->
|
|
|
+ <!-- 当前组件仅支持整数,不支持小数 -->
|
|
|
+ <span class="j-stepper-container">
|
|
|
+ <div class="j-stepper">
|
|
|
+ <el-button
|
|
|
+ class="j-stepper-button"
|
|
|
+ icon="el-icon-minus"
|
|
|
+ plain
|
|
|
+ @click="minusAction"
|
|
|
+ :disabled="minusDisabled"
|
|
|
+ ></el-button>
|
|
|
+ <div class="j-stepper-content">
|
|
|
+ <input
|
|
|
+ :type="integer ? 'tel' : 'text'"
|
|
|
+ v-show="showInput"
|
|
|
+ :name="name"
|
|
|
+ :value="currentValue"
|
|
|
+ :readonly="disableInput"
|
|
|
+ @input="onInput"
|
|
|
+ @focus="onFocus"
|
|
|
+ @blur="onBlur"
|
|
|
+ class="j-stepper-input-inner"
|
|
|
+ />
|
|
|
+ <div class="content-suffix">
|
|
|
+ <slot name="content-suffix-text"></slot>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-button
|
|
|
+ class="j-stepper-button"
|
|
|
+ plain
|
|
|
+ icon="el-icon-plus"
|
|
|
+ @click="plusAction"
|
|
|
+ :disabled="plusDisabled"
|
|
|
+ ></el-button>
|
|
|
+ </div>
|
|
|
+ </span>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { Button } from 'element-ui'
|
|
|
+import { addNumber, formatNumber, equal } from './number'
|
|
|
+
|
|
|
+function isDef (val) {
|
|
|
+ return val !== undefined && val !== null
|
|
|
+}
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'JStepper',
|
|
|
+ components: {
|
|
|
+ [Button.name]: Button
|
|
|
+ },
|
|
|
+ props: {
|
|
|
+ name: [String],
|
|
|
+ value: [Number, String],
|
|
|
+ // 只允许输入整数
|
|
|
+ integer: Boolean,
|
|
|
+ // 是否禁用步进器
|
|
|
+ disabled: Boolean,
|
|
|
+ // 是否禁用增加按钮
|
|
|
+ disablePlus: Boolean,
|
|
|
+ // 是否禁用减少按钮
|
|
|
+ disableMinus: Boolean,
|
|
|
+ // 是否禁用输入框
|
|
|
+ disableInput: Boolean,
|
|
|
+ defaultValue: {
|
|
|
+ type: [Number, String],
|
|
|
+ default: 1
|
|
|
+ },
|
|
|
+ // 步长
|
|
|
+ step: {
|
|
|
+ type: [Number],
|
|
|
+ default: 1
|
|
|
+ },
|
|
|
+ // 最小值
|
|
|
+ min: {
|
|
|
+ type: [Number],
|
|
|
+ default: 1
|
|
|
+ },
|
|
|
+ // 最大值
|
|
|
+ max: {
|
|
|
+ type: [Number],
|
|
|
+ default: Infinity
|
|
|
+ },
|
|
|
+ // 是否显示输入框
|
|
|
+ showInput: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data () {
|
|
|
+ const defaultValue = this.value ?? this.defaultValue
|
|
|
+ const value = this.format(defaultValue)
|
|
|
+
|
|
|
+ if (!equal(value, this.value)) {
|
|
|
+ this.$emit('input', value)
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ type: '',
|
|
|
+ currentValue: value
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ minusDisabled () {
|
|
|
+ return (
|
|
|
+ // +this.min 隐式转换为数字
|
|
|
+ this.disabled || this.disableMinus || this.currentValue <= +this.min
|
|
|
+ )
|
|
|
+ },
|
|
|
+ plusDisabled () {
|
|
|
+ return (
|
|
|
+ this.disabled || this.disablePlus || this.currentValue >= +this.max
|
|
|
+ )
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ max: 'check',
|
|
|
+ min: 'check',
|
|
|
+ integer: 'check',
|
|
|
+ decimalLength: 'check',
|
|
|
+
|
|
|
+ value (val) {
|
|
|
+ if (!equal(val, this.currentValue)) {
|
|
|
+ this.currentValue = this.format(val)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ currentValue (val) {
|
|
|
+ this.$emit('input', val)
|
|
|
+ this.$emit('change', val, { name: this.name })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ minusAction () {
|
|
|
+ this.type = 'minus'
|
|
|
+ this.onChange()
|
|
|
+ },
|
|
|
+ plusAction () {
|
|
|
+ this.type = 'plus'
|
|
|
+ this.onChange()
|
|
|
+ },
|
|
|
+ check () {
|
|
|
+ const val = this.format(this.currentValue)
|
|
|
+ if (!equal(val, this.currentValue)) {
|
|
|
+ this.currentValue = val
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // formatNumber illegal characters
|
|
|
+ formatNumber (value) {
|
|
|
+ return formatNumber(String(value), !this.integer)
|
|
|
+ },
|
|
|
+
|
|
|
+ format (value) {
|
|
|
+ if (this.allowEmpty && value === '') {
|
|
|
+ return value
|
|
|
+ }
|
|
|
+
|
|
|
+ value = this.formatNumber(value)
|
|
|
+
|
|
|
+ // format range
|
|
|
+ value = value === '' ? 0 : +value
|
|
|
+ value = isNaN(value) ? this.min : value
|
|
|
+ value = Math.max(Math.min(this.max, value), this.min)
|
|
|
+
|
|
|
+ // format decimal
|
|
|
+ if (isDef(this.decimalLength)) {
|
|
|
+ value = value.toFixed(this.decimalLength)
|
|
|
+ }
|
|
|
+
|
|
|
+ return value
|
|
|
+ },
|
|
|
+
|
|
|
+ onInput (event) {
|
|
|
+ const { value } = event.target
|
|
|
+
|
|
|
+ let formatted = this.formatNumber(value)
|
|
|
+
|
|
|
+ // limit max decimal length
|
|
|
+ if (isDef(this.decimalLength) && formatted.indexOf('.') !== -1) {
|
|
|
+ const pair = formatted.split('.')
|
|
|
+ formatted = `${pair[0]}.${pair[1].slice(0, this.decimalLength)}`
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!equal(value, formatted)) {
|
|
|
+ event.target.value = formatted
|
|
|
+ }
|
|
|
+
|
|
|
+ // prefer number type
|
|
|
+ if (formatted === String(+formatted)) {
|
|
|
+ formatted = +formatted
|
|
|
+ }
|
|
|
+
|
|
|
+ this.emitChange(formatted)
|
|
|
+ },
|
|
|
+
|
|
|
+ emitChange (value) {
|
|
|
+ if (this.asyncChange) {
|
|
|
+ this.$emit('input', value)
|
|
|
+ this.$emit('change', value, { name: this.name })
|
|
|
+ } else {
|
|
|
+ this.currentValue = value
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ onChange () {
|
|
|
+ const { type } = this
|
|
|
+
|
|
|
+ if (this[`${type}Disabled`]) {
|
|
|
+ this.$emit('overlimit', type)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const diff = type === 'minus' ? -this.step : +this.step
|
|
|
+
|
|
|
+ const value = this.format(addNumber(+this.currentValue, diff))
|
|
|
+
|
|
|
+ this.emitChange(value)
|
|
|
+ this.$emit(type)
|
|
|
+ },
|
|
|
+
|
|
|
+ onFocus (event) {
|
|
|
+ // readonly not work in legacy mobile safari
|
|
|
+ if (this.disableInput && this.$refs.input) {
|
|
|
+ this.$refs.input.blur()
|
|
|
+ } else {
|
|
|
+ this.$emit('focus', event)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ onBlur (event) {
|
|
|
+ const value = this.format(event.target.value)
|
|
|
+ event.target.value = value
|
|
|
+ this.emitChange(value)
|
|
|
+ this.$emit('blur', event)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+$main: #2cb7ca;
|
|
|
+$white: #fff;
|
|
|
+$height: 30px;
|
|
|
+
|
|
|
+.j-stepper-container {
|
|
|
+ display: inline-block;
|
|
|
+ height: $height + 4px;
|
|
|
+ background-color: $white;
|
|
|
+}
|
|
|
+.j-stepper {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ &-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin: 0 2px;
|
|
|
+ padding: 0 8px;
|
|
|
+ height: $height;
|
|
|
+ font-size: 18px;
|
|
|
+ line-height: 26px;
|
|
|
+ color: #171826;
|
|
|
+ border: 1px solid #E0E0E0;
|
|
|
+ }
|
|
|
+ &-input-inner {
|
|
|
+ height: $height;
|
|
|
+ width: 32px;
|
|
|
+ font-size: 14px;
|
|
|
+ text-align: center;
|
|
|
+ background-color: transparent;
|
|
|
+ }
|
|
|
+ &-button {
|
|
|
+ padding: 0;
|
|
|
+ height: $height;
|
|
|
+ width: $height;
|
|
|
+ border: 1px solid #E0E0E0;
|
|
|
+ border-radius: 2px;
|
|
|
+ &:active {
|
|
|
+ border-color: $main;
|
|
|
+ color: $main;
|
|
|
+ background-color: #F7F7F7;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+.content-suffix {
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 22px;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+</style>
|