diff --git a/packages/devui-vue/devui/input-number/__tests__/input-number.spec.tsx b/packages/devui-vue/devui/input-number/__tests__/input-number.spec.tsx index b562c29147..8adc505e0a 100644 --- a/packages/devui-vue/devui/input-number/__tests__/input-number.spec.tsx +++ b/packages/devui-vue/devui/input-number/__tests__/input-number.spec.tsx @@ -1,7 +1,7 @@ import { mount } from '@vue/test-utils'; import { nextTick, ref } from 'vue'; import DInputNumber from '../src/input-number'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; import { Form as DForm, FormItem as DFormItem } from '../../form'; const ns = useNamespace('input-number', true); diff --git a/packages/devui-vue/devui/input-number/index.ts b/packages/devui-vue/devui/input-number/index.ts index 5ff9dae71f..1cb3398869 100644 --- a/packages/devui-vue/devui/input-number/index.ts +++ b/packages/devui-vue/devui/input-number/index.ts @@ -1,5 +1,6 @@ import type { App } from 'vue'; import InputNumber from './src/input-number'; +export * from './src/input-number-types'; export { InputNumber }; @@ -9,5 +10,5 @@ export default { status: '50%', install(app: App): void { app.component(InputNumber.name, InputNumber); - } + }, }; diff --git a/packages/devui-vue/devui/input-number/src/input-number-icons.tsx b/packages/devui-vue/devui/input-number/src/input-number-icons.tsx index 49cc92327b..b9fd0a0e66 100644 --- a/packages/devui-vue/devui/input-number/src/input-number-icons.tsx +++ b/packages/devui-vue/devui/input-number/src/input-number-icons.tsx @@ -1,4 +1,4 @@ -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; const ns = useNamespace('input-number'); diff --git a/packages/devui-vue/devui/input-number/src/input-number-types.ts b/packages/devui-vue/devui/input-number/src/input-number-types.ts index b2e97f09a3..a85bdaf49f 100644 --- a/packages/devui-vue/devui/input-number/src/input-number-types.ts +++ b/packages/devui-vue/devui/input-number/src/input-number-types.ts @@ -1,4 +1,4 @@ -import type { PropType, ExtractPropTypes, ComputedRef, Ref, CSSProperties, InputHTMLAttributes } from 'vue'; +import type { PropType, ExtractPropTypes, ComputedRef, Ref, CSSProperties, InputHTMLAttributes, Prop } from 'vue'; export type ISize = 'lg' | 'md' | 'sm'; @@ -23,7 +23,7 @@ export const inputNumberProps = { default: -Infinity, }, size: { - type: String as PropType + type: String as PropType, }, modelValue: { type: Number, @@ -35,6 +35,9 @@ export const inputNumberProps = { type: [RegExp, String] as PropType, default: '', }, + formatter: { + type: Function as PropType<(val: number | string) => number | string>, + }, } as const; export type InputNumberProps = ExtractPropTypes; diff --git a/packages/devui-vue/devui/input-number/src/input-number.scss b/packages/devui-vue/devui/input-number/src/input-number.scss index 0fc81a6d4c..6e569f048a 100644 --- a/packages/devui-vue/devui/input-number/src/input-number.scss +++ b/packages/devui-vue/devui/input-number/src/input-number.scss @@ -15,6 +15,19 @@ display: flex; border-color: $devui-form-control-line-hover; } + + .#{$devui-prefix}-input-number__input-box--error:not(.disabled) { + border-color: $devui-danger-line; + } + + .#{$devui-prefix}-input-number__control-buttons--error:not(.disabled) { + border-color: $devui-danger-line; + border-left-color: $devui-form-control-line-hover; + + span { + background-color: $devui-danger-bg; + } + } } &:focus-within { @@ -27,13 +40,22 @@ display: flex; border-color: $devui-form-control-line-active; } + + .#{$devui-prefix}-input-number__input-box--error:not(.disabled) { + border-color: $devui-danger-line; + } + + .#{$devui-prefix}-input-number__control-buttons--error:not(.disabled) { + border-color: $devui-danger-line; + border-left-color: $devui-form-control-line-hover; + } } .#{$devui-prefix}-input-number__input-box { box-sizing: border-box; width: 100%; - height: 32px; - line-height: $devui-line-height-base; + height: 28px; + line-height: 20px; padding: 4px 8px; display: block; font-size: $devui-font-size; @@ -120,51 +142,65 @@ } } - .#{$devui-prefix}-input-number__input-wrap { - height: 100%; - } + .#{$devui-prefix}-input-number--lg { + & > .#{$devui-prefix}-input-number__input-box { + height: 46px; + font-size: $devui-font-size-lg; + line-height: 24px; + } - .disabled { - cursor: not-allowed; + .#{$devui-prefix}-input-number__control-buttons .control-button .#{$devui-prefix}-input-number__icon-arrow { + width: 16px; + height: 16px; + } } -} -.#{$devui-prefix}-input-number--lg { - height: 40px; - .#{$devui-prefix}-input-number__input-box { - font-size: $devui-font-size-lg; - height: 100%; + .#{$devui-prefix}-input-number--md { + & > .#{$devui-prefix}-input-number__input-box { + font-size: $devui-font-size; + height: 32px; + line-height: 20px; + } } - .#{$devui-prefix}-input-number__control-buttons .control-button .#{$devui-prefix}-input-number__icon-arrow { - width: 20px; - height: 20px; - } -} + .#{$devui-prefix}-input-number--sm { + & > .#{$devui-prefix}-input-number__input-box { + font-size: $devui-font-size-sm; + line-height: 18px; + height: 26px; + } -.#{$devui-prefix}-input-number--md { - height: 32px; - .#{$devui-prefix}-input-number__input-box { - font-size: $devui-font-size; - height: 100%; + &.#{$devui-prefix}-input-number__control-buttons .control-button { + &:first-child .#{$devui-prefix}-input-number__icon-arrow { + width: 14px; + height: 14px; + } + &:last-child .#{$devui-prefix}-input-number__icon-arrow { + width: 14px; + height: 14px; + } + } } - .#{$devui-prefix}-input-number__control-buttons .control-button .#{$devui-prefix}-input-number__icon-arrow { - width: 18px; - height: 18px; + .#{$devui-prefix}-input-number__input-wrap { + line-height: 100%; } -} -.#{$devui-prefix}-input-number--sm { - height: 24px; + .disabled { + cursor: not-allowed; + } - .#{$devui-prefix}-input-number__input-box { - font-size: $devui-font-size-sm; - height: 100%; + .#{$devui-prefix}-input-number__input-box--error:not(.disabled) { + border-color: $devui-danger-line; + background-color: $devui-danger-bg; } - .#{$devui-prefix}-input-number__control-buttons .control-button .#{$devui-prefix}-input-number__icon-arrow { - width: 16px; - height: 16px; + .#{$devui-prefix}-input-number__control-buttons--error:not(.disabled) { + border-color: $devui-danger-line; + border-left-color: $devui-form-control-line-hover; + + span { + background-color: $devui-danger-bg; + } } } diff --git a/packages/devui-vue/devui/input-number/src/input-number.tsx b/packages/devui-vue/devui/input-number/src/input-number.tsx index 2165c4490e..1aa5d7d88a 100644 --- a/packages/devui-vue/devui/input-number/src/input-number.tsx +++ b/packages/devui-vue/devui/input-number/src/input-number.tsx @@ -1,9 +1,10 @@ -import { defineComponent, toRefs } from 'vue'; +import { defineComponent, toRefs, watch, inject } from 'vue'; import type { SetupContext } from 'vue'; import { inputNumberProps, InputNumberProps } from './input-number-types'; import { IncIcon, DecIcon } from './input-number-icons'; import { useRender, useEvent, useExpose } from './use-input-number'; import './input-number.scss'; +import { FORM_ITEM_TOKEN, FormItemContext } from '@devui/shared/components/form'; export default defineComponent({ name: 'DInputNumber', @@ -14,6 +15,14 @@ export default defineComponent({ const { wrapClass, customStyle, otherAttrs, controlButtonsClass, inputWrapClass, inputInnerClass } = useRender(props, ctx); const { inputRef } = useExpose(ctx); const { inputVal, minDisabled, maxDisabled, onAdd, onSubtract, onInput, onChange } = useEvent(props, ctx, inputRef); + const formItemContext = inject(FORM_ITEM_TOKEN, undefined) as FormItemContext; + + watch( + () => props.modelValue, + () => { + formItemContext?.validate('change').catch(() => {}); + } + ); return () => (
@@ -35,6 +44,7 @@ export default defineComponent({ {...otherAttrs} onInput={onInput} onChange={onChange} + onBlur={() => formItemContext?.validate('blur').catch(() => {})} />
diff --git a/packages/devui-vue/devui/input-number/src/use-input-number.ts b/packages/devui-vue/devui/input-number/src/use-input-number.ts index ee845be730..7816822d7d 100644 --- a/packages/devui-vue/devui/input-number/src/use-input-number.ts +++ b/packages/devui-vue/devui/input-number/src/use-input-number.ts @@ -1,9 +1,8 @@ import { computed, reactive, toRefs, watch, ref, inject } from 'vue'; import type { SetupContext, Ref, CSSProperties } from 'vue'; import { InputNumberProps, UseEvent, UseRender, IState, UseExpose } from './input-number-types'; -import { useNamespace } from '../../shared/hooks/use-namespace'; -import { isNumber, isUndefined } from '../../shared/utils'; -import { FORM_TOKEN } from '../../form'; +import { isNumber, isUndefined, useNamespace } from '@devui/shared/utils'; +import { FORM_ITEM_TOKEN, FORM_TOKEN, FormItemContext } from '@devui/shared/components/form'; const ns = useNamespace('input-number'); @@ -12,27 +11,33 @@ export function useRender(props: InputNumberProps, ctx: SetupContext): UseRender const { style, class: customClass, ...otherAttrs } = ctx.attrs; const customStyle = { style: style as CSSProperties }; + const formItemContext = inject(FORM_ITEM_TOKEN, undefined) as FormItemContext; + const isValidateError = computed(() => formItemContext?.validateState === 'error'); + const inputNumberSize = computed(() => props.size || formContext?.size || 'md'); const wrapClass = computed(() => [ { [ns.b()]: true, - [ns.m(inputNumberSize.value)]: true, }, customClass, ]); const controlButtonsClass = computed(() => ({ [ns.e('control-buttons')]: true, + [ns.em('control-buttons', 'error')]: isValidateError.value, disabled: props.disabled, + [ns.m(inputNumberSize.value)]: true, })); const inputWrapClass = computed(() => ({ [ns.e('input-wrap')]: true, + [ns.m(inputNumberSize.value)]: true, })); const inputInnerClass = computed(() => ({ [ns.e('input-box')]: true, + [ns.em('input-box', 'error')]: isValidateError.value, disabled: props.disabled, })); @@ -85,7 +90,7 @@ export function useEvent(props: InputNumberProps, ctx: SetupContext, inputRef: R const inputVal = computed(() => { if (!isUndefined(state.userInputValue)) { - return state.userInputValue; + return props.formatter ? props.formatter(state.userInputValue ?? 0) : state.userInputValue; } let currentValue = state.currentValue; if (currentValue === '' || isUndefined(currentValue) || Number.isNaN(currentValue)) { @@ -95,7 +100,7 @@ export function useEvent(props: InputNumberProps, ctx: SetupContext, inputRef: R // todo 小数精度 确认是否应该以正则处理 currentValue = currentValue.toFixed(numPrecision.value); } - return currentValue; + return props.formatter ? props.formatter(currentValue ?? 0) : currentValue; }); const toPrecision = (num: number) => { @@ -187,11 +192,22 @@ export function useEvent(props: InputNumberProps, ctx: SetupContext, inputRef: R ); const onInput = (event: Event) => { - state.userInputValue = (event.target as HTMLInputElement).value; + const value = (event.target as HTMLInputElement).value; + if (value[0] === '-') { + state.userInputValue = '-' + value.substring(1).replace(/[^0-9.]/g, ''); + } else { + state.userInputValue = value.replace(/[^0-9.]/g, ''); + } + inputRef.value.value = props.formatter ? props.formatter(state.userInputValue) : state.userInputValue; }; - const onChange = (event: Event) => { - setCurrentValue((event.target as HTMLInputElement).value); + const onChange = () => { + const value = state.userInputValue; + const newVal = value !== '' ? Number(value) : ''; + if ((isNumber(newVal) && !Number.isNaN(newVal)) || value === '') { + setCurrentValue(newVal); + } + state.userInputValue = undefined; }; return { inputVal, minDisabled, maxDisabled, onAdd, onSubtract, onInput, onChange }; diff --git a/packages/devui-vue/docs/components/input-number/index.md b/packages/devui-vue/docs/components/input-number/index.md index 2f32e27072..bac1d8219a 100644 --- a/packages/devui-vue/docs/components/input-number/index.md +++ b/packages/devui-vue/docs/components/input-number/index.md @@ -190,7 +190,7 @@ export default defineComponent({
reg
- +
regStr
@@ -208,7 +208,43 @@ export default defineComponent({ num1, num2, reg, - regStr + regStr, + }; + }, +}); + + +``` + +::: + +### 格式化 + +:::demo 通过`formatter`参数来格式化输入框中的值。 + +```vue + +