From 1aed43f61f1b35b47f653c7a6751dcd4e29391bd Mon Sep 17 00:00:00 2001 From: GreatZP <greatzp@greatzp.cn> Date: Sun, 20 Aug 2023 17:41:41 +0800 Subject: [PATCH 1/3] refactor(input-number): input-number --- .../__tests__/input-number.spec.tsx | 2 +- .../input-number/src/input-number-icons.tsx | 2 +- .../input-number/src/input-number-types.ts | 7 ++-- .../devui/input-number/src/input-number.scss | 35 +++++++++++++++++++ .../devui/input-number/src/input-number.tsx | 12 ++++++- .../input-number/src/use-input-number.ts | 19 +++++++--- .../docs/components/input-number/index.md | 27 +++++++------- 7 files changed, 82 insertions(+), 22 deletions(-) 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/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<ISize> + type: String as PropType<ISize>, }, modelValue: { type: Number, @@ -35,6 +35,9 @@ export const inputNumberProps = { type: [RegExp, String] as PropType<RegExp | string>, default: '', }, + formatter: { + type: Function as PropType<(val: number | string) => number | string>, + }, } as const; export type InputNumberProps = ExtractPropTypes<typeof inputNumberProps>; 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..d5a5421004 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,6 +40,15 @@ 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 { @@ -167,4 +189,17 @@ width: 16px; height: 16px; } + + .#{$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; + } + } } 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..d331c7321d 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, { FORM_ITEM_TOKEN, FormItemContext } from '../../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 () => ( <div class={wrapClass.value} {...customStyle}> @@ -35,6 +44,7 @@ export default defineComponent({ {...otherAttrs} onInput={onInput} onChange={onChange} + onBlur={() => formItemContext?.validate('blur').catch(() => {})} /> </div> </div> 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..f30c285e87 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 @@ -3,7 +3,7 @@ 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 { FORM_ITEM_TOKEN, FORM_TOKEN, FormItemContext } from '../../form'; const ns = useNamespace('input-number'); @@ -12,6 +12,9 @@ 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(() => [ @@ -24,6 +27,7 @@ export function useRender(props: InputNumberProps, ctx: SetupContext): UseRender const controlButtonsClass = computed(() => ({ [ns.e('control-buttons')]: true, + [ns.em('control-buttons', 'error')]: isValidateError.value, disabled: props.disabled, })); @@ -33,6 +37,7 @@ export function useRender(props: InputNumberProps, ctx: SetupContext): UseRender 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,7 +192,13 @@ 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) => { diff --git a/packages/devui-vue/docs/components/input-number/index.md b/packages/devui-vue/docs/components/input-number/index.md index 2f32e27072..e2bf09039a 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({ <div> <div class="space">reg</div> <d-input-number v-model="num1" :reg="reg"></d-input-number> - + <div class="space">regStr</div> <d-input-number v-model="num2" :reg="regStr"></d-input-number> </div> @@ -208,7 +208,7 @@ export default defineComponent({ num1, num2, reg, - regStr + regStr, }; }, }); @@ -225,17 +225,18 @@ export default defineComponent({ ### InputNumber 参数 -| 参数名 | 类型 | 默认值 | 说明 | 跳转 Demo | -|:------------|:----------------|:-----------|:-------------------|:-------------------| -| v-model | `number` | -- | 可选,文本框的值 | [基本用法](#基本用法) | -| step | `number` | 1 | 可选,步数 | [步数](#步数) | -| placeholder | `string` | -- | 可选,文本框 placeholder | [基本用法](#基本用法) | -| max | `number` | Infinity | 可选,输入框的最大值 max | [数值范围](#数值范围) | -| min | `number` | -Infinity | 可选,输入框的最小值 min | [数值范围](#数值范围) | -| disabled | `boolean` | false | 可选,文本框是否被禁用 | [禁用状态](#禁用状态) | -| precision | `number` | -- | 可选,数值精度 | [精度](#精度) | -| size | [ISize](#isize) | 'md' | 可选,文本框尺寸 | [尺寸](#尺寸) | -| reg | `RegExp\| string` | -- | 可选,用于限制输入的正则或正则字符串 | [正则限制](#正则限制)| +| 参数名 | 类型 | 默认值 | 说明 | 跳转 Demo | +| :---------- | :-------------------------------------------- | :-------- | :----------------------------------- | :-------------------- | +| v-model | `number` | -- | 可选,文本框的值 | [基本用法](#基本用法) | +| step | `number` | 1 | 可选,步数 | [步数](#步数) | +| placeholder | `string` | -- | 可选,文本框 placeholder | [基本用法](#基本用法) | +| max | `number` | Infinity | 可选,输入框的最大值 max | [数值范围](#数值范围) | +| min | `number` | -Infinity | 可选,输入框的最小值 min | [数值范围](#数值范围) | +| disabled | `boolean` | false | 可选,文本框是否被禁用 | [禁用状态](#禁用状态) | +| precision | `number` | -- | 可选,数值精度 | [精度](#精度) | +| size | [ISize](#isize) | 'md' | 可选,文本框尺寸 | [尺寸](#尺寸) | +| reg | `RegExp\| string` | -- | 可选,用于限制输入的正则或正则字符串 | [正则限制](#正则限制) | +| formatter | `(val: number \| string) => number \| string` | -- | 可选,用来格式化输入框显示内容 | | ### InputNumber 事件 From 52e62de604b442f90e8e6f5fef827d37bfaf3dc8 Mon Sep 17 00:00:00 2001 From: GreatZP <greatzp@greatzp.cn> Date: Tue, 29 Aug 2023 16:52:22 +0800 Subject: [PATCH 2/3] refactor: input-number --- .../docs/components/input-number/index.md | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/devui-vue/docs/components/input-number/index.md b/packages/devui-vue/docs/components/input-number/index.md index e2bf09039a..bac1d8219a 100644 --- a/packages/devui-vue/docs/components/input-number/index.md +++ b/packages/devui-vue/docs/components/input-number/index.md @@ -223,6 +223,42 @@ export default defineComponent({ ::: +### 格式化 + +:::demo 通过`formatter`参数来格式化输入框中的值。 + +```vue +<template> + <div> + <d-input-number v-model="formatterNum" :formatter="formatter" placeholder="请输入"></d-input-number> + </div> +</template> +<script> +import { defineComponent, ref } from 'vue'; + +export default defineComponent({ + setup() { + const formatterNum = ref(0); + const formatter = (val) => { + return val ? `${val}%` : '0%'; + } + return { + formatterNum, + formatter, + }; + }, +}); +</script> +<style> +.space { + padding: 5px 0; + font-size: 16px; +} +</style> +``` + +::: + ### InputNumber 参数 | 参数名 | 类型 | 默认值 | 说明 | 跳转 Demo | @@ -236,7 +272,7 @@ export default defineComponent({ | precision | `number` | -- | 可选,数值精度 | [精度](#精度) | | size | [ISize](#isize) | 'md' | 可选,文本框尺寸 | [尺寸](#尺寸) | | reg | `RegExp\| string` | -- | 可选,用于限制输入的正则或正则字符串 | [正则限制](#正则限制) | -| formatter | `(val: number \| string) => number \| string` | -- | 可选,用来格式化输入框显示内容 | | +| formatter | `(val: number \| string) => number \| string` | -- | 可选,用来格式化输入框显示内容 | [格式化](#格式化) | ### InputNumber 事件 From e5eb124bd093fe74d9805cc9616add763b6de773 Mon Sep 17 00:00:00 2001 From: GreatZP <greatzp@greatzp.cn> Date: Mon, 4 Sep 2023 16:14:18 +0800 Subject: [PATCH 3/3] refactor(input-number): input-number --- .../devui-vue/devui/input-number/index.ts | 3 +- .../devui/input-number/src/input-number.scss | 77 ++++++++++--------- .../devui/input-number/src/input-number.tsx | 2 +- .../input-number/src/use-input-number.ts | 17 ++-- 4 files changed, 53 insertions(+), 46 deletions(-) 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.scss b/packages/devui-vue/devui/input-number/src/input-number.scss index d5a5421004..6e569f048a 100644 --- a/packages/devui-vue/devui/input-number/src/input-number.scss +++ b/packages/devui-vue/devui/input-number/src/input-number.scss @@ -54,8 +54,8 @@ .#{$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; @@ -142,56 +142,57 @@ } } - .#{$devui-prefix}-input-number__input-wrap { - height: 100%; - } - - .disabled { - cursor: not-allowed; - } -} + .#{$devui-prefix}-input-number--lg { + & > .#{$devui-prefix}-input-number__input-box { + height: 46px; + font-size: $devui-font-size-lg; + line-height: 24px; + } -.#{$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__control-buttons .control-button .#{$devui-prefix}-input-number__icon-arrow { + width: 16px; + height: 16px; + } } - .#{$devui-prefix}-input-number__control-buttons .control-button .#{$devui-prefix}-input-number__icon-arrow { - width: 20px; - height: 20px; + .#{$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--md { - height: 32px; - .#{$devui-prefix}-input-number__input-box { - font-size: $devui-font-size; - height: 100%; - } + .#{$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__control-buttons .control-button .#{$devui-prefix}-input-number__icon-arrow { - width: 18px; - height: 18px; + &.#{$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--sm { - height: 24px; - - .#{$devui-prefix}-input-number__input-box { - font-size: $devui-font-size-sm; - height: 100%; + .#{$devui-prefix}-input-number__input-wrap { + line-height: 100%; } - .#{$devui-prefix}-input-number__control-buttons .control-button .#{$devui-prefix}-input-number__icon-arrow { - width: 16px; - height: 16px; + .disabled { + cursor: not-allowed; } .#{$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--error:not(.disabled) { 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 d331c7321d..1aa5d7d88a 100644 --- a/packages/devui-vue/devui/input-number/src/input-number.tsx +++ b/packages/devui-vue/devui/input-number/src/input-number.tsx @@ -4,7 +4,7 @@ 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, { FORM_ITEM_TOKEN, FormItemContext } from '../../form'; +import { FORM_ITEM_TOKEN, FormItemContext } from '@devui/shared/components/form'; export default defineComponent({ name: 'DInputNumber', 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 f30c285e87..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_ITEM_TOKEN, FORM_TOKEN, FormItemContext } 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'); @@ -20,7 +19,6 @@ export function useRender(props: InputNumberProps, ctx: SetupContext): UseRender const wrapClass = computed(() => [ { [ns.b()]: true, - [ns.m(inputNumberSize.value)]: true, }, customClass, ]); @@ -29,10 +27,12 @@ export function useRender(props: InputNumberProps, ctx: SetupContext): UseRender [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(() => ({ @@ -201,8 +201,13 @@ export function useEvent(props: InputNumberProps, ctx: SetupContext, inputRef: R 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 };