fix: 🐛 优化 InputNumbe 处理中间状态值的逻辑,支持配置不立即响应输入变化 (#1116)

This commit is contained in:
不如摸鱼去 2025-06-22 12:12:11 +08:00 committed by GitHub
parent cb408f553a
commit ff99b22a69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1647 additions and 328 deletions

View File

@ -49,6 +49,17 @@ function handleChange({ value }) {
<wd-input-number v-model="value" @change="handleChange" disable-input />
```
## 禁用按钮
可以单独禁用增加或减少按钮。
```html
<!-- 禁用减号按钮 -->
<wd-input-number v-model="value" @change="handleChange" disable-minus />
<!-- 禁用加号按钮 -->
<wd-input-number v-model="value" @change="handleChange" disable-plus />
```
## 无输入框
@ -97,6 +108,49 @@ function handleChange({ value }) {
}
```
## 非立即更新模式
设置 `immediate-change``false`,输入框内容变化时不会立即触发 `change` 事件,仅在失焦或点击按钮时触发。
```html
<!-- 立即更新模式(默认) -->
<wd-input-number v-model="value1" @change="handleChange" :immediate-change="true" />
<!-- 非立即更新模式 -->
<wd-input-number v-model="value2" @change="handleChange" :immediate-change="false" />
```
```typescript
const value1 = ref<number>(1)
const value2 = ref<number>(1)
function handleChange({ value }) {
console.log(value)
}
```
## 初始化时自动更新
设置 `update-on-init` 属性控制是否在初始化时更新 `v-model` 为修正后的值。
- 当 `update-on-init="true"`(默认)时,会将初始值修正到符合 `min``max``step``precision` 等规则的有效值,并同步更新 `v-model`
- 当 `update-on-init="false"` 时,保持初始值不修正(不改变 `v-model`),但仍会进行显示格式化(如精度处理)
```html
<!-- 自动更新初始值(默认) -->
<wd-input-number v-model="value1" @change="handleChange" :update-on-init="true" :min="3" :max="15" :step="2" step-strictly />
<!-- 不更新初始值,保持原始值 -->
<wd-input-number v-model="value2" @change="handleChange" :update-on-init="false" :min="3" :max="15" :step="2" step-strictly />
```
```typescript
const value1 = ref<number>(1) // 会自动修正为4≥3的最小2的倍数
const value2 = ref<number>(1) // 保持为1不会修正但会格式化显示
function handleChange({ value }) {
console.log(value)
}
```
## 异步变更
通过 `before-change` 可以在输入值变化前进行校验和拦截。
@ -153,6 +207,9 @@ const beforeChange: InputNumberBeforeChange = (value) => {
| adjustPosition | 原生属性,键盘弹起时,是否自动上推页面 | boolean | - | true | 1.3.11 |
| before-change | 输入框值改变前触发,返回 false 会阻止输入框值改变,支持返回 `Promise` | `(value: number \| string) => boolean \| Promise<boolean>` | - | - | 1.6.0 |
| long-press | 是否允许长按进行加减 | boolean | - | false | 1.8.0 |
| immediate-change | 是否立即响应输入变化false 时仅在失焦和按钮点击时更新 | boolean | - | true | $LOWEST_VERSION$ |
| update-on-init | 是否在初始化时更新 v-model 为修正后的值 | boolean | - | true | $LOWEST_VERSION$ |
| input-type | 输入框类型 | string | number / digit | digit | $LOWEST_VERSION$ |
## Events

View File

@ -49,6 +49,18 @@ Set the `disable-input` property.
<wd-input-number v-model="value" @change="handleChange" disable-input />
```
## Disable Buttons
You can disable the increase or decrease buttons individually.
```html
<!-- Disable minus button -->
<wd-input-number v-model="value" @change="handleChange" disable-minus />
<!-- Disable plus button -->
<wd-input-number v-model="value" @change="handleChange" disable-plus />
```
## Without Input Box
Set `without-input` to hide the input box.
@ -96,6 +108,49 @@ function handleChange({ value }) {
}
```
## Non-Immediate Update Mode
Set `immediate-change` to `false`, the `change` event will not be triggered immediately when the input box content changes, only when it loses focus or buttons are clicked.
```html
<!-- Immediate update mode (default) -->
<wd-input-number v-model="value1" @change="handleChange" :immediate-change="true" />
<!-- Non-immediate update mode -->
<wd-input-number v-model="value2" @change="handleChange" :immediate-change="false" />
```
```typescript
const value1 = ref<number>(1)
const value2 = ref<number>(1)
function handleChange({ value }) {
console.log(value)
}
```
## Auto-update on Initialization
Set the `update-on-init` property to control whether to update the `v-model` with the corrected value during initialization.
- When `update-on-init="true"` (default), the initial value will be corrected to comply with `min`, `max`, `step`, `precision` and other rules, and the `v-model` will be updated synchronously
- When `update-on-init="false"`, the initial value will not be corrected (v-model unchanged), but display formatting (such as precision) will still be applied
```html
<!-- Auto-update initial value (default) -->
<wd-input-number v-model="value1" @change="handleChange" :update-on-init="true" :min="3" :max="15" :step="2" step-strictly />
<!-- Don't update initial value, keep original value -->
<wd-input-number v-model="value2" @change="handleChange" :update-on-init="false" :min="3" :max="15" :step="2" step-strictly />
```
```typescript
const value1 = ref<number>(1) // Will be auto-corrected to 4 (minimum multiple of 2 that is ≥3)
const value2 = ref<number>(1) // Remains 1, will not be corrected but will be formatted for display
function handleChange({ value }) {
console.log(value)
}
```
## Asynchronous Change
Through `before-change`, you can validate and intercept before the input value changes.
@ -152,6 +207,9 @@ Set the `long-press` property to allow long press for increment/decrement.
| adjustPosition | Native property, whether to automatically push up the page when keyboard pops up | boolean | - | true | 1.3.11 |
| before-change | Triggered before input box value changes, returning false will prevent input box value from changing, supports returning `Promise` | `(value: number \| string) => boolean \| Promise<boolean>` | - | - | 1.6.0 |
| long-press | Whether to allow long press for increment/decrement | boolean | - | false | 1.8.0 |
| immediate-change | Whether to respond to input changes immediately, false will only update on blur and button clicks | boolean | - | true | $LOWEST_VERSION$ |
| update-on-init | Whether to update v-model with corrected value during initialization | boolean | - | true | $LOWEST_VERSION$ |
| input-type | Input field type | string | number / digit | digit | $LOWEST_VERSION$ |
## Events

View File

@ -15,6 +15,12 @@
<demo-block :title="$t('jin-yong-shu-ru-kuang')">
<wd-input-number v-model="value10" @change="handleChange4" disable-input />
</demo-block>
<demo-block title="禁用减号按钮">
<wd-input-number v-model="value13" @change="handleChange13" disable-minus />
</demo-block>
<demo-block title="禁用加号按钮">
<wd-input-number v-model="value14" @change="handleChange14" disable-plus />
</demo-block>
<demo-block :title="$t('wu-shu-ru-kuang')">
<view class="flex">
<view>{{ $t('shu-liang-value5') }}{{ value5 }}</view>
@ -27,12 +33,55 @@
<demo-block :title="$t('shu-ru-yan-ge-wei-bu-shu-de-bei-shu')">
<wd-input-number v-model="value7" @change="handleChange7" step-strictly :step="2" />
</demo-block>
<demo-block title="严格步进+边界限制">
<view class="strict-bounds-demo">
<view class="demo-description">{{ value19 }}步进值2最小值3最大值15严格步进模式</view>
<wd-input-number v-model="value19" @change="handleChange19" step-strictly :step="2" :min="3" :max="15" />
<view class="demo-note">
尝试输入各种值
<br />
输入1 自动调整为43的最小2的倍数
<br />
输入5 自动调整为4最接近的2的倍数
<br />
输入17 自动调整为1415的最大2的倍数
</view>
</view>
</demo-block>
<demo-block :title="$t('xiu-gai-shu-ru-kuang-kuan-du')">
<wd-input-number v-model="value8" input-width="70px" @change="handleChange8" />
</demo-block>
<demo-block :title="$t('yun-xu-kong-zhi-bing-she-zhi-placeholder')">
<wd-input-number v-model="value9" allow-null :placeholder="$t('bu-xian')" input-width="70px" @change="handleChange9" />
</demo-block>
<demo-block title="非允许空值但可临时删除">
<view class="temp-empty-demo">
<view class="demo-description">{{ value18 }}可以删除为空但失焦时会自动修正为最小值</view>
<wd-input-number v-model="value18" @change="handleChange18" :allow-null="false" :min="1" />
<view class="demo-note">尝试删除输入框中的所有内容然后点击其他地方失焦会自动修正为最小值1</view>
</view>
</demo-block>
<demo-block title="键盘弹起不上推页面">
<wd-input-number v-model="value15" @change="handleChange15" :adjust-position="false" />
</demo-block>
<demo-block title="非立即更新模式">
<view class="immediate-demo">
<view class="demo-title">立即更新模式默认- {{ value16 }}</view>
<wd-input-number v-model="value16" @change="handleChange16" :immediate-change="true" />
<view class="demo-title">非立即更新模式 - {{ value17 }}</view>
<wd-input-number v-model="value17" @change="handleChange17" :immediate-change="false" />
<view class="demo-note">在输入框中输入内容时上方的值会立即更新下方的值仅在失焦或点击按钮时更新</view>
</view>
</demo-block>
<demo-block title="初始化时自动修正">
<view class="format-init-demo">
<view class="demo-title">自动修正初始值 - {{ value20 }}</view>
<wd-input-number v-model="value20" @change="handleChange20" :update-on-init="true" :min="3" :max="15" :step="2" step-strictly />
<view class="demo-title">不修正初始值 - {{ value21 }}</view>
<wd-input-number v-model="value21" @change="handleChange21" :update-on-init="false" :min="3" :max="15" :step="2" step-strictly />
<view class="demo-note">上方组件会在初始化时自动将值1修正为43的最小2的倍数下方组件不会自动修正</view>
</view>
</demo-block>
<demo-block :title="$t('yi-bu-bian-geng')">
<wd-input-number v-model="value11" :before-change="beforeChange" />
</demo-block>
@ -43,7 +92,7 @@
</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 { type InputNumberBeforeChange } from '@/uni_modules/wot-design-uni/components/wd-input-number/types'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { loading, close } = useToast()
@ -61,35 +110,71 @@ const value9 = ref<string>('')
const value10 = ref<number>(1)
const value11 = ref<number>(1)
const value12 = ref<number>(1)
const value13 = ref<number>(1)
const value14 = ref<number>(1)
const value15 = ref<number>(1)
const value16 = ref<number>(1)
const value17 = ref<number>(1)
const value18 = ref<number>(1)
const value19 = ref<number>(4)
const value20 = ref<number>(1)
const value21 = ref<number>(1)
function handleChange1({ value }: any) {
function handleChange1({ value }: { value: number | string }) {
console.log(value)
}
function handleChange2({ value }: any) {
function handleChange2({ value }: { value: number | string }) {
console.log(value)
}
function handleChange3({ value }: any) {
function handleChange3({ value }: { value: number | string }) {
console.log(value)
}
function handleChange4({ value }: any) {
function handleChange4({ value }: { value: number | string }) {
console.log(value)
}
function handleChange5({ value }: any) {
function handleChange5({ value }: { value: number | string }) {
console.log(value)
}
function handleChange6({ value }: any) {
function handleChange6({ value }: { value: number | string }) {
console.log(value)
}
function handleChange7({ value }: any) {
function handleChange7({ value }: { value: number | string }) {
console.log(value)
}
function handleChange8({ value }: any) {
function handleChange8({ value }: { value: number | string }) {
console.log(value)
}
function handleChange9({ value }: any) {
function handleChange9({ value }: { value: number | string }) {
console.log(value)
}
function handleChange12({ value }: any) {
function handleChange12({ value }: { value: number | string }) {
console.log(value)
}
function handleChange13({ value }: { value: number | string }) {
console.log(value)
}
function handleChange14({ value }: { value: number | string }) {
console.log(value)
}
function handleChange15({ value }: { value: number | string }) {
console.log(value)
}
function handleChange16({ value }: { value: number | string }) {
console.log(value)
}
function handleChange17({ value }: { value: number | string }) {
console.log(value)
}
function handleChange18({ value }: { value: number | string }) {
console.log(value)
}
function handleChange19({ value }: { value: number | string }) {
console.log(value)
}
function handleChange20({ value }: { value: number | string }) {
console.log(value)
}
function handleChange21({ value }: { value: number | string }) {
console.log(value)
}
@ -109,4 +194,96 @@ const beforeChange: InputNumberBeforeChange = (value) => {
justify-content: space-between;
align-items: center;
}
.immediate-demo {
.demo-title {
font-size: 14px;
color: #333;
margin-bottom: 8px;
font-weight: 500;
}
.demo-note {
font-size: 12px;
color: #999;
margin-top: 12px;
line-height: 1.4;
padding: 8px;
background: #f5f5f5;
border-radius: 4px;
}
.wd-input-number {
margin-bottom: 16px;
}
}
.temp-empty-demo {
.demo-description {
font-size: 14px;
color: #333;
margin-bottom: 8px;
font-weight: 500;
}
.demo-note {
font-size: 12px;
color: #999;
margin-top: 12px;
line-height: 1.4;
padding: 8px;
background: #f5f5f5;
border-radius: 4px;
}
.wd-input-number {
margin-bottom: 16px;
}
}
.strict-bounds-demo {
.demo-description {
font-size: 14px;
color: #333;
margin-bottom: 8px;
font-weight: 500;
}
.demo-note {
font-size: 12px;
color: #999;
margin-top: 12px;
line-height: 1.4;
padding: 8px;
background: #f5f5f5;
border-radius: 4px;
}
.wd-input-number {
margin-bottom: 16px;
}
}
.format-init-demo {
.demo-title {
font-size: 14px;
color: #333;
margin-bottom: 8px;
font-weight: 500;
}
.demo-note {
font-size: 12px;
color: #999;
margin-top: 12px;
line-height: 1.4;
padding: 8px;
background: #f5f5f5;
border-radius: 4px;
}
.wd-input-number {
margin-bottom: 16px;
}
}
</style>

View File

@ -1,13 +1,13 @@
/*
* @Author: weisheng
* @Date: 2024-03-15 20:40:34
* @LastEditTime: 2025-02-19 12:47:54
* @LastEditTime: 2025-06-21 18:23:35
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-input-number/types.ts
*
*/
import type { PropType } from 'vue'
import type { ExtractPropTypes, PropType } from 'vue'
import { baseProps, makeBooleanProp, makeNumberProp, makeNumericProp, makeRequiredProp, makeStringProp, numericProp } from '../common/props'
/**
@ -19,20 +19,6 @@ export type InputNumberBeforeChange = (value: number | string) => boolean | Prom
export type OperationType = 'add' | 'sub'
/**
*
* Input: 用户输入事件
* Blur: 失焦事件
* Watch: 监听值变化事件
* Button: 按钮点击事件
*/
export enum InputNumberEventType {
Input = 'input',
Blur = 'blur',
Watch = 'watch',
Button = 'button'
}
export const inputNumberProps = {
...baseProps,
/**
@ -102,5 +88,23 @@ export const inputNumberProps = {
/**
*
*/
longPress: makeBooleanProp(false)
longPress: makeBooleanProp(false),
/**
* false
*/
immediateChange: makeBooleanProp(true),
/**
* v-model
* true: v-model
* false:
*/
updateOnInit: makeBooleanProp(true),
/**
*
* number:
* digit: 整数输入
*/
inputType: makeStringProp<'number' | 'digit'>('digit')
}
export type InputNumberProps = ExtractPropTypes<typeof inputNumberProps>

View File

@ -14,7 +14,8 @@
<input
class="wd-input-number__input"
:style="`${inputWidth ? 'width: ' + inputWidth : ''}`"
type="digit"
:type="inputType"
:input-mode="precision ? 'decimal' : 'numeric'"
:disabled="disabled || disableInput"
:value="String(inputValue)"
:placeholder="placeholder"
@ -52,263 +53,410 @@ export default {
import wdIcon from '../wd-icon/wd-icon.vue'
import { computed, nextTick, ref, watch } from 'vue'
import { isDef, isEqual } from '../common/util'
import { inputNumberProps, InputNumberEventType, type OperationType } from './types'
import { inputNumberProps, type OperationType } 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()) //
let longPressTimer: ReturnType<typeof setTimeout> | null = null //
const emit = defineEmits<{
/**
* 数值变化事件
*/
(e: 'change', value: { value: number | string }): void
/**
* 输入框聚焦事件
*/
(e: 'focus', detail: any): void
/**
* 输入框失焦事件
*/
(e: 'blur', value: { value: string | number }): void
/**
* v-model 更新事件
*/
(e: 'update:modelValue', value: number | string): void
}>()
const inputValue = ref<string | number>(getInitValue())
let longPressTimer: ReturnType<typeof setTimeout> | null = null
/**
* 判断数字是否达到最小值限制
*/
const minDisabled = computed(() => {
const value = formatValue(inputValue.value)
const { disabled, min, step } = props
return disabled || Number(value) <= min || changeStep(value, -step) < min
const val = toNumber(inputValue.value)
return props.disabled || val <= props.min || addStep(val, -props.step) < props.min
})
/**
* 判断数字是否达到最大值限制
*/
const maxDisabled = computed(() => {
const value = formatValue(inputValue.value)
const { disabled, max, step } = props
return disabled || Number(value) >= max || changeStep(value, step) > max
const val = toNumber(inputValue.value)
return props.disabled || val >= props.max || addStep(val, props.step) > props.max
})
// modelValue
watch(
() => props.modelValue,
(value) => {
updateValue(value, InputNumberEventType.Watch)
(val) => {
inputValue.value = formatValue(val)
}
)
// max, min, precision
// max, min, precision
watch([() => props.max, () => props.min, () => props.precision], () => {
const value = formatValue(inputValue.value)
updateValue(value, InputNumberEventType.Watch)
const val = toNumber(inputValue.value)
inputValue.value = formatValue(val)
})
/**
* 对比两个值是否相等
* @param value1 第一个值
* @param value2 第二个值
* @returns 是否相等
* 获取初始值
*/
function isValueEqual(value1: number | string, value2: number | string) {
return isEqual(String(value1), String(value2))
}
//
function getInitValue() {
if (!props.updateOnInit) {
//
return formatDisplay(props.modelValue)
}
const formatted = formatValue(props.modelValue)
if (!isValueEqual(formatted, props.modelValue)) {
//
if (!isEqual(String(formatted), String(props.modelValue))) {
emit('update:modelValue', formatted)
}
return formatted
}
//
function toPrecision(value: number) {
const precision = Number(props.precision)
return Number(parseFloat(`${Math.round(value * Math.pow(10, precision)) / Math.pow(10, precision)}`).toFixed(precision))
}
/**
* 获取数字的小数位数
* @param value 需要计算精度的数字
* @returns 小数位数
*/
function getPrecision(value?: number) {
if (!isDef(value)) return 0
const valueString = value.toString()
const dotPosition = valueString.indexOf('.')
let precision = 0
if (dotPosition !== -1) {
precision = valueString.length - dotPosition - 1
}
return precision
function getPrecision(val?: number) {
if (!isDef(val)) return 0
const str = val.toString()
const dotIndex = str.indexOf('.')
return dotIndex === -1 ? 0 : str.length - dotIndex - 1
}
/**
* 按步进值严格递增或递减
* @param value 当前值
* @returns 按步进值调整后的值
* 按指定精度处理数值
*/
function toStrictlyStep(value: number | string) {
const stepPrecision = getPrecision(props.step)
const precisionFactory = Math.pow(10, stepPrecision)
return (Math.round(Number(value) / props.step) * precisionFactory * props.step) / precisionFactory
}
//
function doUpdate(value: string | number) {
inputValue.value = value
const formatted = formatValue(value)
nextTick(() => {
inputValue.value = formatted
emit('update:modelValue', inputValue.value)
emit('change', { value: inputValue.value })
})
function toPrecision(val: number) {
const precision = Number(props.precision)
return Math.round(val * Math.pow(10, precision)) / Math.pow(10, precision)
}
/**
* 清理输入字符串中多余的小数点
* @param value 输入的字符串
* @returns 清理后的字符串
* 将字符串或数字转换为标准数值
*/
function cleanExtraDecimal(value: string): string {
const precisionAllowed = Number(props.precision) > 0
if (precisionAllowed) {
const dotIndex = value.indexOf('.')
if (dotIndex === -1) {
return value
} else {
const integerPart = value.substring(0, dotIndex + 1)
// '.'
const decimalPart = value.substring(dotIndex + 1).replace(/\./g, '')
return integerPart + decimalPart
}
} else {
//
const dotIndex = value.indexOf('.')
return dotIndex !== -1 ? value.substring(0, dotIndex) : value
}
}
/**
* 更新输入框的值
* @param value 新的值
* @param eventType 触发更新的事件类型
*/
function updateValue(value: string | number, eventType: InputNumberEventType = InputNumberEventType.Input) {
const fromUser = eventType !== InputNumberEventType.Watch // watch
const forceFormat = eventType === InputNumberEventType.Blur || eventType === InputNumberEventType.Button
// Input Watch '.'
if ((eventType === InputNumberEventType.Input || eventType === InputNumberEventType.Watch) && String(value).endsWith('.') && props.precision) {
inputValue.value = value
nextTick(() => {
inputValue.value = cleanExtraDecimal(String(value))
emit('update:modelValue', inputValue.value)
emit('change', { value: inputValue.value })
})
return
}
if (!forceFormat && isValueEqual(value, inputValue.value)) {
return
function toNumber(val: string | number): number {
//
if (props.allowNull && (!isDef(val) || val === '')) {
return NaN
}
const update = () => doUpdate(value)
if (fromUser) {
callInterceptor(props.beforeChange, {
args: [value],
done: update
})
} else {
update()
}
}
//
function changeStep(val: string | number, step: number) {
val = Number(val)
if (isNaN(val)) {
if (!isDef(val) || val === '') {
return props.min
}
const precision = Math.max(getPrecision(val), getPrecision(step))
const precisionFactor = Math.pow(10, precision)
return toPrecision((val * precisionFactor + step * precisionFactor) / precisionFactor)
let str = String(val)
//
if (str.endsWith('.')) str = str.slice(0, -1)
if (str.startsWith('.')) str = '0' + str
if (str.startsWith('-.')) str = '-0' + str.substring(1)
if (str === '-' || str === '') return props.min
let num = Number(str)
if (isNaN(num)) num = props.min
return normalizeValue(num)
}
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, InputNumberEventType.Button)
/**
* 标准化数值应用步进边界精度规则
*/
function normalizeValue(val: number): number {
let result = val
//
if (props.stepStrictly) {
const stepPrecision = getPrecision(props.step)
const factor = Math.pow(10, stepPrecision)
result = (Math.round(result / props.step) * factor * props.step) / factor
}
//
if (props.stepStrictly) {
result = applyStrictBounds(result, props.min, props.max)
} else {
result = Math.min(Math.max(result, props.min), props.max)
}
//
if (isDef(props.precision)) {
result = toPrecision(result)
}
return result
}
//
/**
* 严格步进模式下的边界处理
*/
function applyStrictBounds(val: number, min: number, max: number): number {
if (val >= min && val <= max) return val
const stepPrecision = getPrecision(props.step)
const factor = Math.pow(10, stepPrecision)
if (val < min) {
const minSteps = Math.ceil((min * factor) / (props.step * factor))
const candidate = toPrecision((minSteps * props.step * factor) / factor)
if (candidate > max) {
const maxSteps = Math.floor((max * factor) / (props.step * factor))
return toPrecision((maxSteps * props.step * factor) / factor)
}
return candidate
}
if (val > max) {
const maxSteps = Math.floor((max * factor) / (props.step * factor))
const candidate = toPrecision((maxSteps * props.step * factor) / factor)
if (candidate < min) {
const minSteps = Math.ceil((min * factor) / (props.step * factor))
return toPrecision((minSteps * props.step * factor) / factor)
}
return candidate
}
return val
}
/**
* 格式化值用于显示包含修正逻辑
*/
function formatValue(val: string | number): string | number {
if (props.allowNull && (!isDef(val) || val === '')) {
return ''
}
const num = toNumber(val)
const precision = Number(props.precision)
if (!isDef(props.precision)) {
return num
}
return precision === 0 ? Number(num.toFixed(0)) : num.toFixed(precision)
}
/**
* 仅做显示格式化不包含值修正逻辑
*/
function formatDisplay(val: string | number): string | number {
if (props.allowNull && (!isDef(val) || val === '')) {
return ''
}
if (!isDef(val) || val === '') {
return props.min
}
let num = Number(val)
if (isNaN(num)) {
return props.min
}
const precision = Number(props.precision)
if (!isDef(props.precision)) {
return num
}
return precision === 0 ? Number(num.toFixed(0)) : num.toFixed(precision)
}
/**
* 检查是否为中间输入状态
*/
function isIntermediate(val: string): boolean {
if (!val) return false
const str = String(val)
return str.endsWith('.') || str.startsWith('.') || str.startsWith('-.') || str === '-' || (Number(props.precision) > 0 && str.indexOf('.') === -1)
}
/**
* 清理输入值
*/
function cleanInput(val: string): string {
if (!val) return ''
//
let cleaned = val.replace(/[^\d.-]/g, '')
//
const hasNegative = cleaned.startsWith('-')
cleaned = cleaned.replace(/-/g, '')
if (hasNegative) cleaned = '-' + cleaned
//
const precision = Number(props.precision)
if (precision > 0) {
const parts = cleaned.split('.')
if (parts.length > 2) {
cleaned = parts[0] + '.' + parts.slice(1).join('')
}
} else {
cleaned = cleaned.split('.')[0]
}
//
if (cleaned.startsWith('.')) return '0' + cleaned
if (cleaned.startsWith('-.')) return '-0' + cleaned.substring(1)
return cleaned
}
/**
* 更新值并触发事件
*/
function updateValue(val: string | number) {
//
if (props.allowNull && (!isDef(val) || val === '')) {
if (isEqual('', String(props.modelValue))) {
inputValue.value = ''
return
}
const doUpdate = () => {
inputValue.value = ''
emit('update:modelValue', '')
emit('change', { value: '' })
}
callInterceptor(props.beforeChange, { args: [''], done: doUpdate })
return
}
const num = toNumber(val)
const display = formatValue(val)
if (isEqual(String(num), String(props.modelValue))) {
inputValue.value = display
return
}
const doUpdate = () => {
inputValue.value = display
emit('update:modelValue', num)
emit('change', { value: num })
}
callInterceptor(props.beforeChange, { args: [num], done: doUpdate })
}
/**
* 按步进值增减
*/
function addStep(val: string | number, step: number) {
const num = Number(val)
if (isNaN(num)) return normalizeValue(props.min)
const precision = Math.max(getPrecision(num), getPrecision(step))
const factor = Math.pow(10, precision)
const result = (num * factor + step * factor) / factor
return normalizeValue(result)
}
/**
* 处理按钮点击
*/
function handleClick(type: OperationType) {
const diff = type === 'add' ? props.step : -props.step
changeValue(diff)
const step = type === 'add' ? props.step : -props.step
if ((step < 0 && (minDisabled.value || props.disableMinus)) || (step > 0 && (maxDisabled.value || props.disablePlus))) return
const newVal = addStep(inputValue.value, step)
updateValue(newVal)
}
/**
* 处理输入事件
*/
function handleInput(event: any) {
const rawValue = event.detail.value || ''
updateValue(rawValue, InputNumberEventType.Input)
const rawVal = event.detail.value || ''
//
inputValue.value = rawVal
nextTick(() => {
//
if (rawVal === '') {
inputValue.value = ''
if (props.immediateChange && props.allowNull) {
updateValue('')
}
return
}
//
const cleaned = cleanInput(rawVal)
//
if (Number(props.precision) > 0 && isIntermediate(cleaned)) {
inputValue.value = cleaned
return
}
//
inputValue.value = cleaned
if (props.immediateChange) {
updateValue(cleaned)
}
})
}
/**
* 处理失焦事件
*/
function handleBlur(event: any) {
const value = event.detail.value || ''
updateValue(value, InputNumberEventType.Blur)
emit('blur', { value })
const val = event.detail.value || ''
updateValue(val)
emit('blur', { value: val })
}
//
/**
* 处理聚焦事件
*/
function handleFocus(event: any) {
emit('focus', event.detail)
}
/**
* 长按逻辑
*/
function longPressStep(type: OperationType) {
clearlongPressTimer()
clearLongPressTimer()
longPressTimer = setTimeout(() => {
handleClick(type)
longPressStep(type)
}, 250)
}
//
function handleTouchStart(type: OperationType) {
if (!props.longPress) return
clearlongPressTimer()
clearLongPressTimer()
longPressTimer = setTimeout(() => {
handleClick(type)
longPressStep(type)
}, 600)
}
//
function handleTouchEnd() {
if (!props.longPress) return
clearlongPressTimer()
clearLongPressTimer()
}
//
function clearlongPressTimer() {
function clearLongPressTimer() {
if (longPressTimer) {
clearTimeout(longPressTimer)
longPressTimer = null
}
}
//
function handleFocus(event: any) {
emit('focus', event.detail)
}
//
function formatValue(value: string | number) {
if (props.allowNull && (!isDef(value) || value === '')) {
return ''
}
let formatted = Number(value)
if (isNaN(formatted)) {
formatted = props.min
}
if (props.stepStrictly) {
formatted = toStrictlyStep(value)
}
formatted = Math.min(Math.max(formatted, props.min), props.max)
if (isDef(props.precision)) {
formatted = toPrecision(formatted)
}
return formatted
}
</script>
<style lang="scss" scoped>

File diff suppressed because it is too large Load Diff