mirror of
https://gitee.com/wot-design-uni/wot-design-uni.git
synced 2025-12-06 17:18:40 +08:00
fix: 🐛 优化 InputNumbe 处理中间状态值的逻辑,支持配置不立即响应输入变化 (#1116)
This commit is contained in:
parent
cb408f553a
commit
ff99b22a69
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 → 自动调整为4(≥3的最小2的倍数)
|
||||
<br />
|
||||
• 输入5 → 自动调整为4(最接近的2的倍数)
|
||||
<br />
|
||||
• 输入17 → 自动调整为14(≤15的最大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修正为4(≥3的最小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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 toPrecision(val: number) {
|
||||
const precision = Number(props.precision)
|
||||
return Math.round(val * Math.pow(10, precision)) / Math.pow(10, precision)
|
||||
}
|
||||
|
||||
// 内部更新处理函数
|
||||
function doUpdate(value: string | number) {
|
||||
inputValue.value = value
|
||||
const formatted = formatValue(value)
|
||||
/**
|
||||
* 将字符串或数字转换为标准数值
|
||||
*/
|
||||
function toNumber(val: string | number): number {
|
||||
// 空值处理
|
||||
if (props.allowNull && (!isDef(val) || val === '')) {
|
||||
return NaN
|
||||
}
|
||||
|
||||
if (!isDef(val) || val === '') {
|
||||
return props.min
|
||||
}
|
||||
|
||||
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 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 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 rawVal = event.detail.value || ''
|
||||
|
||||
// 立即更新显示
|
||||
inputValue.value = rawVal
|
||||
|
||||
nextTick(() => {
|
||||
inputValue.value = formatted
|
||||
emit('update:modelValue', inputValue.value)
|
||||
emit('change', { value: inputValue.value })
|
||||
// 空值处理
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理输入字符串中多余的小数点
|
||||
* @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
|
||||
}
|
||||
function handleBlur(event: any) {
|
||||
const val = event.detail.value || ''
|
||||
updateValue(val)
|
||||
emit('blur', { value: val })
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新输入框的值
|
||||
* @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
|
||||
}
|
||||
|
||||
const update = () => doUpdate(value)
|
||||
|
||||
if (fromUser) {
|
||||
callInterceptor(props.beforeChange, {
|
||||
args: [value],
|
||||
done: update
|
||||
})
|
||||
} else {
|
||||
update()
|
||||
}
|
||||
function handleFocus(event: any) {
|
||||
emit('focus', event.detail)
|
||||
}
|
||||
|
||||
// 根据步进值改变值
|
||||
function changeStep(val: string | number, step: number) {
|
||||
val = Number(val)
|
||||
if (isNaN(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)
|
||||
}
|
||||
|
||||
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 handleClick(type: OperationType) {
|
||||
const diff = type === 'add' ? props.step : -props.step
|
||||
changeValue(diff)
|
||||
}
|
||||
|
||||
function handleInput(event: any) {
|
||||
const rawValue = event.detail.value || ''
|
||||
updateValue(rawValue, InputNumberEventType.Input)
|
||||
}
|
||||
|
||||
function handleBlur(event: any) {
|
||||
const value = event.detail.value || ''
|
||||
updateValue(value, InputNumberEventType.Blur)
|
||||
emit('blur', { value })
|
||||
}
|
||||
|
||||
// 每隔一段时间,重新调用自身,达到长按加减效果
|
||||
/**
|
||||
* 长按逻辑
|
||||
*/
|
||||
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
Loading…
x
Reference in New Issue
Block a user