feat: 修复 InputNumber 在设置为 allow-null 时被赋值为空时未触发更新的问题并支持异步更新 (#812)

This commit is contained in:
不如摸鱼去 2025-01-02 00:36:03 +08:00 committed by GitHub
parent 583acc2fa9
commit 0fc90ddcc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 173 additions and 93 deletions

View File

@ -96,6 +96,34 @@ function handleChange1({ value }) {
}
```
## 异步变更
通过 `before-change` 可以在输入值变化前进行校验和拦截。
```html
<wd-input-number v-model="value" :before-change="beforeChange" />
```
```typescript
import { ref } from 'vue'
import { useToast } from '@/uni_modules/wot-design-uni'
import type { InputNumberBeforeChange } from '@/uni_modules/wot-design-uni/components/wd-input-number/types'
const { loading, close } = useToast()
const value = ref<number>(1)
const beforeChange: InputNumberBeforeChange = (value) => {
loading({ msg: `正在更新到${value}...` })
return new Promise((resolve) => {
setTimeout(() => {
close()
resolve(true)
}, 500)
})
}
```
## Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
@ -109,12 +137,13 @@ function handleChange1({ value }) {
| disabled | 禁用 | boolean | - | false | - |
| without-input | 不显示输入框 | boolean | - | false | - |
| input-width | 输入框宽度 | string | - | 36px | - |
| allow-null | 允许空值 | boolean | - | false | - |
| allow-null | 是否允许输入的值为空,设置为 `true` 后允许传入空字符串 | boolean | - | false | - |
| placeholder | 占位文本 | string | - | - | - |
| disable-input | 禁用输入框 | boolean | - | false | 0.2.14 |
| disable-plus | 禁用增加按钮 | boolean | - | false | 0.2.14 |
| disable-minus | 禁用减少按钮 | boolean | - | false | 0.2.14 |
| adjustPosition | 原生属性,键盘弹起时,是否自动上推页面 | boolean | - | true | 1.3.11 |
| before-change | 输入框值改变前触发,返回 false 会阻止输入框值改变,支持返回 `Promise` | `(value: number \| string) => boolean \| Promise<boolean>` | - | - | $LOWEST_VERSION$ |
## Events

View File

@ -33,10 +33,16 @@
<demo-block title="允许空值,并设置 placeholder">
<wd-input-number v-model="value9" allow-null placeholder="不限" input-width="70px" @change="handleChange9" />
</demo-block>
<demo-block title="异步变更">
<wd-input-number v-model="value11" :before-change="beforeChange" />
</demo-block>
</page-wraper>
</template>
<script lang="ts" setup>
import { useToast } from '@/uni_modules/wot-design-uni'
import type { InputNumberBeforeChange } from '@/uni_modules/wot-design-uni/components/wd-input-number/types'
import { ref } from 'vue'
const { loading, close } = useToast()
const value1 = ref<number>(1)
const value2 = ref<number>(1)
@ -48,6 +54,7 @@ const value7 = ref<number>(1)
const value8 = ref<number>(2)
const value9 = ref<string>('')
const value10 = ref<number>(1)
const value11 = ref<number>(1)
function handleChange1({ value }: any) {
console.log(value)
@ -76,6 +83,16 @@ function handleChange8({ value }: any) {
function handleChange9({ value }: any) {
console.log(value)
}
const beforeChange: InputNumberBeforeChange = (value) => {
loading({ msg: `正在更新到${value}...` })
return new Promise((resolve) => {
setTimeout(() => {
close()
resolve(true)
}, 500)
})
}
</script>
<style lang="scss" scoped>
.flex {

View File

@ -0,0 +1,43 @@
import { isPromise } from './util'
function noop() {}
export type Interceptor = (...args: any[]) => Promise<boolean> | boolean | undefined | void
export function callInterceptor(
interceptor: Interceptor | undefined,
{
args = [],
done,
canceled,
error
}: {
args?: unknown[]
done: () => void
canceled?: () => void
error?: () => void
}
) {
if (interceptor) {
// eslint-disable-next-line prefer-spread
const returnVal = interceptor.apply(null, args)
if (isPromise(returnVal)) {
returnVal
.then((value) => {
if (value) {
done()
} else if (canceled) {
canceled()
}
})
.catch(error || noop)
} else if (returnVal) {
done()
} else if (canceled) {
canceled()
}
} else {
done()
}
}

View File

@ -1,14 +1,17 @@
/*
* @Author: weisheng
* @Date: 2024-03-15 20:40:34
* @LastEditTime: 2024-09-18 09:49:12
* @LastEditTime: 2024-12-31 00:33:21
* @LastEditors: weisheng
* @Description:
* @FilePath: \wot-design-uni\src\uni_modules\wot-design-uni\components\wd-input-number\types.ts
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-input-number/types.ts
*
*/
import type { PropType } from 'vue'
import { baseProps, makeBooleanProp, makeNumberProp, makeNumericProp, makeRequiredProp, makeStringProp, numericProp } from '../common/props'
export type InputNumberBeforeChange = (value: number | string) => boolean | Promise<boolean>
export const inputNumberProps = {
...baseProps,
/**
@ -70,5 +73,9 @@ export const inputNumberProps = {
/**
*
*/
adjustPosition: makeBooleanProp(true)
adjustPosition: makeBooleanProp(true),
/**
* `false` `Promise`
*/
beforeChange: Function as PropType<InputNumberBeforeChange>
}

View File

@ -9,7 +9,7 @@
:style="`${inputWidth ? 'width: ' + inputWidth : ''}`"
type="digit"
:disabled="disabled || disableInput"
v-model="inputValue"
:value="String(inputValue)"
:placeholder="placeholder"
:adjust-position="adjustPosition"
@input="handleInput"
@ -37,57 +37,51 @@ export default {
<script lang="ts" setup>
import wdIcon from '../wd-icon/wd-icon.vue'
import { ref, watch } from 'vue'
import { debounce, isDef, isEqual } from '../common/util'
import { computed, nextTick, ref, watch } from 'vue'
import { isDef, isEqual } from '../common/util'
import { inputNumberProps } from './types'
import { callInterceptor } from '../common/interceptor'
const props = defineProps(inputNumberProps)
const emit = defineEmits(['focus', 'blur', 'change', 'update:modelValue'])
const inputValue = ref<string | number>(getInitValue()) //
const minDisabled = ref<boolean>(false)
const maxDisabled = ref<boolean>(false)
const inputValue = ref<string | number>('') //
//
const minDisabled = computed(() => {
const value = formatValue(inputValue.value)
const { disabled, min, step } = props
return disabled || Number(value) <= min || changeStep(value, -step) < min
})
//
const maxDisabled = computed(() => {
const value = formatValue(inputValue.value)
const { disabled, max, step } = props
return disabled || Number(value) >= max || changeStep(value, step) > max
})
watch(
() => props.modelValue,
(newValue) => {
inputValue.value = newValue
splitDisabled(newValue)
},
{ immediate: true, deep: true }
(value) => {
updateValue(value)
}
)
watch(
[() => props.max, () => props.min],
() => {
updateBoundary()
},
{ immediate: true, deep: true }
)
watch([() => props.max, () => props.min, () => props.precision], () => {
const value = formatValue(inputValue.value)
updateValue(value)
})
watch(
() => props.disabled,
(newValue) => {
minDisabled.value = newValue
maxDisabled.value = newValue
},
{ immediate: true, deep: true }
)
function updateBoundary() {
debounce(() => {
const value = formatValue(inputValue.value)
if (!isEqual(inputValue.value, value)) {
setValue(value)
}
splitDisabled(value)
}, 30)()
function isValueEqual(value1: number | string, value2: number | string) {
return isEqual(String(value1), String(value2))
}
function splitDisabled(value: number | string) {
const { disabled, min, max, step } = props
minDisabled.value = disabled || Number(value) <= min || changeStep(value, -step) < min
maxDisabled.value = disabled || Number(value) >= max || changeStep(value, step) > max
function getInitValue() {
const formatted = formatValue(props.modelValue)
if (!isValueEqual(formatted, props.modelValue)) {
emit('update:modelValue', formatted)
}
return formatted
}
function toPrecision(value: number) {
@ -95,7 +89,7 @@ function toPrecision(value: number) {
}
function getPrecision(value?: number) {
if (value === undefined) return 0
if (!isDef(value)) return 0
const valueString = value.toString()
const dotPosition = valueString.indexOf('.')
let precision = 0
@ -111,103 +105,93 @@ function toStrictlyStep(value: number | string) {
return (Math.round(Number(value) / props.step) * precisionFactory * props.step) / precisionFactory
}
function setValue(value: string | number, change: boolean = true) {
if (props.allowNull && (!isDef(value) || value === '')) {
dispatchChangeEvent(value, change)
function updateValue(value: string | number, fromUser: boolean = false) {
if (isValueEqual(value, inputValue.value)) {
return
}
if (props.stepStrictly) {
value = toStrictlyStep(value)
const update = () => {
inputValue.value = value
const formatted = formatValue(value)
nextTick(() => {
inputValue.value = formatted
emit('update:modelValue', inputValue.value)
emit('change', { value: inputValue.value })
})
}
if ((value || value === 0) && props.precision !== undefined) {
value = toPrecision(Number(value))
}
if (Number(value) > props.max) value = toPrecision(props.max)
if (Number(value) < props.min) value = toPrecision(props.min)
dispatchChangeEvent(value, change)
if (fromUser) {
callInterceptor(props.beforeChange, {
args: [value],
done: update
})
} else {
update()
}
}
function changeStep(val: string | number, step: number) {
val = Number(val)
if (isNaN(val)) {
return props.min
}
const precisionFactory = Math.pow(10, props.precision)
return toPrecision((val * precisionFactory + step * precisionFactory) / precisionFactory)
}
function sub() {
if (minDisabled.value || props.disableMinus) return
function changeValue(step: number) {
if ((step < 0 && (minDisabled.value || props.disableMinus)) || (step > 0 && (maxDisabled.value || props.disablePlus))) return
const value = changeStep(inputValue.value, step)
updateValue(value, true)
}
const newValue = changeStep(inputValue.value, -props.step)
dispatchChangeEvent(newValue)
function sub() {
changeValue(-props.step)
}
function add() {
if (maxDisabled.value || props.disablePlus) return
const newValue = changeStep(inputValue.value, props.step)
dispatchChangeEvent(newValue)
changeValue(props.step)
}
function handleInput(event: any) {
const value = event.detail.value || ''
dispatchChangeEvent(value)
updateValue(value, true)
}
function handleFocus(event: any) {
emit('focus', event.detail)
}
function handleBlur() {
const value = formatValue(inputValue.value)
if (!isEqual(inputValue.value, value)) {
setValue(value)
}
function handleBlur(event: any) {
const value = event.detail.value || ''
updateValue(value, true)
emit('blur', {
value
})
}
function dispatchChangeEvent(value: string | number, change: boolean = true) {
if (isEqual(inputValue.value, value)) {
return
}
inputValue.value = value
change && emit('update:modelValue', inputValue.value)
change && emit('change', { value })
}
function formatValue(value: string | number) {
if (props.allowNull && (!isDef(value) || value === '')) {
return ''
}
let formatValue = Number(value)
let formatted = Number(value)
if (isNaN(formatValue)) {
value = props.min
if (isNaN(formatted)) {
formatted = props.min
}
if (props.stepStrictly) {
formatValue = toStrictlyStep(value)
formatted = toStrictlyStep(value)
}
if (props.precision !== undefined) {
formatValue = Number(formatValue.toFixed(props.precision))
}
if (formatValue > props.max) {
formatValue = props.max
}
if (formatValue < props.min) {
formatValue = props.min
formatted = Math.min(Math.max(formatted, props.min), props.max)
if (isDef(props.precision)) {
formatted = Number(formatted.toFixed(props.precision))
}
return formatValue
return formatted
}
</script>