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 };