Browse Source

price单元测试、input部分新增功能 (#1076)

* fix: price单元测试、input部分新增功能
ailululu 3 years ago
parent
commit
208a019ca8

+ 5 - 1
src/packages/__VUE/formitem/index.vue

@@ -15,7 +15,7 @@
 </template>
 <script lang="ts">
 import { pxCheck } from '../../utils/pxCheck';
-import { computed, inject, PropType, ref } from 'vue';
+import { computed, inject, provide, PropType, ref } from 'vue';
 import { createComponent } from '../../utils/create';
 const { componentName, create } = createComponent('form-item');
 import { FormItemRule } from './types';
@@ -71,6 +71,10 @@ export default create({
   setup(props, { emit }) {
     const parent = inject('formErrorTip') as any;
 
+    provide('form', {
+      props
+    });
+
     const labelStyle = computed(() => {
       return {
         width: pxCheck(props.labelWidth),

+ 121 - 32
src/packages/__VUE/input/demo.vue

@@ -1,27 +1,87 @@
 <template>
   <div class="demo full">
+    <!-- <h2>校验</h2>
+    <nut-form>
+    <nut-form-item label="姓名" prop="name" required :rules="[{ required: true, message: '请填写姓名' }]">
+      <nut-input v-model="state.val0" @change="change" @focus="focus" @blur="blur" label="文本(异步)" />
+    </nut-form-item>
+    </nut-form> -->
     <h2>基础用法</h2>
-    <nut-input v-model="state.val0" @change="change" @focus="focus" @blur="blur" label="文本(异步)" />
+    <nut-input placeholder="请输入文本" v-model="state.val1" label="文本" />
+
+    <h2>自定义类型</h2>
+    <nut-input label="文本" v-model="state.text" />
+    <nut-input label="密码" v-model="state.password" type="password" />
+    <nut-input label="数字" v-model="state.number" type="number" placeholder="支持小数点的输入" />
+    <nut-input label="整数" v-model="state.digit" type="digit" />
+    <nut-input label="手机号" v-model="state.tel" type="tel" />
+
+    <h2>禁用输入框</h2>
+    <nut-input v-model="state.readonly" readonly label="文本" placeholder="输入框只读" />
+    <nut-input v-model="state.disabled" disabled label="文本" placeholder="输入框已禁用" />
+
+    <h2>显示图标</h2>
+    <nut-input v-model="state.showIcon" label="文本" left-icon="dongdong" right-icon="ask2" placeholder="显示图标" />
+    <nut-input v-model="state.clear" label="文本" clearable clearSize="14" placeholder="显示清除图标" />
+
+    <h2>错误提示</h2>
+    <nut-input v-model="state.required" label="文本" required placeholder="必填项" />
+    <nut-input v-model="state.error1" label="文本" error placeholder="输入内容标红" />
+    <nut-input v-model="state.error2" label="文本" error-message="底部错误提示文案" placeholder="底部错误提示文案" />
+
+    <h2>插入按钮</h2>
+    <nut-input v-model="state.buttonVal" clearable center label="短信验证码" placeholder="请输入短信验证码">
+      <template #button>
+        <nut-button size="small" type="primary"> 发送验证码 </nut-button>
+      </template>
+    </nut-input>
+
+    <h2>格式化输入内容</h2>
+    <nut-input v-model="state.format1" label="文本" :formatter="formatter" placeholder="在输入时执行格式化" />
     <nut-input
-      placeholder="请输入文本"
-      @change="change"
-      v-model="state.val1"
-      :require-show="true"
+      v-model="state.format2"
       label="文本"
-      @clear="clear"
+      :formatter="formatter"
+      format-trigger="onBlur"
+      placeholder="在失焦时执行格式化"
     />
-    <h2>禁用输入框</h2>
-    <nut-input v-model="state.val2" @change="change" :disabled="true" label="文本" />
-    <nut-input v-model="state.val3" @change="change" :readonly="true" label="文本" />
-    <h2>限制输入长度</h2>
-    <nut-input v-model="state.val4" @change="change" max-length="7" label="限制7" />
-    <h2>自定义类型</h2>
-    <nut-input v-model="state.val5" @change="change" type="password" label="密码" />
-    <nut-input v-model="state.val6" @change="change" type="number" label="整数" />
-    <nut-input v-model="state.val7" @change="change" type="digit" placeholder="支持小数点的输入" label="数字" />
+
+    <h2>显示字数统计</h2>
+    <nut-input
+      v-model="state.textarea"
+      label="留言"
+      type="textarea"
+      show-word-limit
+      rows="2"
+      maxNum="50"
+      placeholder="请输入留言"
+    />
+
+    <h2>对齐方式</h2>
+    <nut-input v-model="state.align1" label="文本" label-align="right" placeholder="文本内容对齐" />
+    <nut-input v-model="state.align2" label="文本" input-align="right" placeholder="输入框内容对齐" />
+
     <h2>无边框</h2>
-    <nut-input v-model="state.val8" @change="change" :hasBorder="false" label="无边框" />
-    <nut-input v-model="state.val9" @change="change" :hasBorder="false" label="无边框" />
+    <nut-input v-model="state.disabled" :border="false" label="无边框" />
+    <nut-input v-model="state.showIcon" :border="false" label="无边框" />
+
+    <h2>点击事件</h2>
+    <nut-input
+      v-model="state.event2"
+      label="event"
+      left-icon="dongdong"
+      right-icon="ask2"
+      clearable
+      placeholder="显示图标"
+      @update:model-value="change"
+      @focus="focus"
+      @blur="blur"
+      @clear="clear"
+      @click="click"
+      @click-input="clickInput"
+      @click-left-icon="clickLeftIcon"
+      @click-right-icon="clickRightIcon"
+    />
   </div>
 </template>
 
@@ -32,22 +92,32 @@ const { createDemo } = createComponent('input');
 export default createDemo({
   setup() {
     const state = reactive({
-      val0: '初始数据',
       val1: '',
-      val2: '禁止修改',
-      val3: 'readonly 只读',
-      val4: '',
-      val5: '',
-      val6: '',
-      val7: '',
-      val8: '',
-      val9: ''
+      text: '',
+      password: '',
+      number: '',
+      digit: '',
+      tel: '',
+      readonly: '',
+      disabled: '',
+      showIcon: '',
+      required: '',
+      error1: '',
+      error2: '',
+      buttonVal: '',
+      format1: '',
+      format2: '',
+      textarea: '',
+      align1: '',
+      align2: '',
+      event1: '',
+      event2: ''
     });
     setTimeout(function () {
-      state.val0 = '异步数据';
+      // state.val0 = '异步数据';
     }, 2000);
-    const change = (value: string | number, event: Event) => {
-      console.log('change: ', value, event);
+    const change = (value: string | number) => {
+      console.log('change: ', value);
     };
     const focus = (value: string | number, event: Event) => {
       console.log('focus:', value, event);
@@ -55,15 +125,34 @@ export default createDemo({
     const blur = (value: string | number, event: Event) => {
       console.log('blur:', value, event);
     };
-    const clear = (value: string | number) => {
-      console.log('clear:', value);
+    const clear = (value: string | number, event: Event) => {
+      console.log('clear:', value, event);
+    };
+    const click = (value: string | number) => {
+      console.log('click:', value);
+    };
+    const clickInput = (value: string | number) => {
+      console.log('clickInput:', value);
     };
+    const clickLeftIcon = (value: string | number) => {
+      console.log('clickLeftIcon:', value);
+    };
+    const clickRightIcon = (value: string | number) => {
+      console.log('clickRightIcon:', value);
+    };
+    const formatter = (value: string) => value.replace(/\d/g, '');
+
     return {
       state,
       change,
       blur,
       clear,
-      focus
+      focus,
+      click,
+      clickInput,
+      clickLeftIcon,
+      clickRightIcon,
+      formatter
     };
   }
 });

+ 36 - 14
src/packages/__VUE/input/doc.md

@@ -213,25 +213,47 @@ app.use(Icon);
 | 参数         | 说明                                   | 类型           | 默认值  |
 |--------------|----------------------------------------|----------------|---------|
 | v-model      | 输入值,双向绑定                       | String         | -       |
-| type         | 类型,可选值为 `text` `number`  等     | String         | `text`  |
-| placeholder  | 为空时占位符                           | String         | -       |
-| label        | 左侧文案                               | String         | -       |
-| require-show | 左侧*号是否展示                        | Boolean        | `false` |
-| has-border   | 下边框是否展示                         | Boolean        | `true` |
-| disabled     | 是否禁用                               | Boolean        | `false` |
-| readonly     | 是否只读                               | Boolean        | `false` |
-| max-length   | 限制最长输入字符                       | String、Number | -       |
-| clearable    | 展示清除icon                           | Boolean        | `true`  |
-| text-align   | 文本位置,可选值`left`,`center`,`right` | String         | `left`  |
+| type         | 输入框类型,支持原生 `input` 标签的所有 `type` 属性,另外还支持 `textarea` `number` `digit`     | String         | `text`  |
+| placeholder  | 输入框为空时占位符                      | String         | -       |
+| label        | 左侧文本                              | String         | -       |
+| label-class  | 左侧文本额外类名                        | String | -  |
+| label-width  | 左侧文本宽度,默认单位为 `px`            | String、Number | `80`    |
+| label-align  | 左侧文本对齐方式,可选值 `left`、`center`、`right`   | String | `left` |
+| input-align  | 输入框内容对齐方式,可选值 `left`、`center`、`right` | String | `left` |
+| colon        | 是否在 label 后面添加冒号               | Boolean        | `false` |
+| required     | 左侧*号是否展示                        | Boolean        | `false` |
+| border       | 是否显示下边框                         | Boolean        | `true` |
+| disabled     | 是否禁用                              | Boolean        | `false` |
+| readonly     | 是否只读                              | Boolean        | `false` |
+| autofocus    | 是否自动获得焦点,iOS 系统不支持该属性     | Boolean        | `false` |
+| max-num      | 限制最长输入字符                       | String、Number  | -       |
+| clearable    | 展示清除 Icon                         | Boolean        | `false`  |
+| clear-icon   | 清除图标 Icon 名称或图片链接,可参考 Icon 组件的 name 属性           | String        | `mask-close`  |
+| clear-size   | 清除图标的 `font-size` 大小           | String        | `14`  |
+| left-icon    | 左侧 Icon 名称或图片链接,可参考 Icon 组件的 name 属性 | String        | - |
+| right-icon   | 右侧 Icon 名称或图片链接,可参考 Icon 组件的 name 属性 | String        | - |
+| left-size    | 左侧 Icon 的 `font-size` 大小           | String        | `14`  |
+| right-size   | 右侧 Icon 的 `font-size` 大小           | String        | `14`  |
+| show-word-limit | 是否显示限制最长输入字符,需要设置 `max-num` 属性 | Boolean | `false`  |
+| error         | 是否标红                                | Boolean | `false`  |
+| error-message | 底部错误提示文案,为空时不展示            | String、Number | - |
+| error-message-align | 底部错误提示文案对齐方式,可选值 `left`、`center`、`right`          | String | - |
+| formatter      | 输入内容格式化函数    | `(val: string) => string` | - |
+| format-trigger | 格式化函数触发的时机,可选值为 `onChange`、`onBlur` | String | - |
 
 ### Event
 
 | 名称   | 说明           | 回调参数    |
 |--------|----------------|-------------|
-| change | 输入内容时触发 | val ,event  |
-| focus  | 聚焦时触发     | val  ,event |
-| blur   | 失焦时触发     | val ,event  |
-| clear  | 点击清空时触发 | val         |
+| update:model-value | 输入框内容变化时触发 | val  |
+| focus  | 输入框聚焦时触发     | val  ,event |
+| blur   | 输入框失焦时触发     | val ,event  |
+| clear  | 点击清除按钮时触发   | val ,event  |
+| click  | 点击组件时触发      | val ,event  |
+| click-input      | 点击输入区域时触发      | val ,event  |
+| click-left-icon  | 点击左侧图标时触发      | val ,event  |
+| click-right-icon | 点击右侧图标时触发      | val ,event  |
+
 
 
 

+ 67 - 24
src/packages/__VUE/input/index.scss

@@ -1,55 +1,93 @@
+input,
+textarea {
+  font: inherit;
+}
 .nut-input {
   position: relative;
   width: 100%;
-  padding: 10px 0px 10px 25px;
+  padding: 10px 25px;
   display: flex;
+  line-height: 24px;
   background: $white;
   font-size: $input-font-size;
   box-sizing: border-box;
-
+  &.center {
+    align-items: center;
+  }
   &.nut-input-border {
     border-bottom: 1px solid $input-border-bottom;
   }
 
-  &.nut-input-require {
-    &::before {
-      position: absolute;
-      left: 14px;
-      color: $input-require-color;
-      content: '*';
-    }
-  }
-
   .input-text,
   &__text--readonly {
-    width: 90%;
-    flex: 1;
-    padding: 0 10px;
-    padding-right: 35px;
+    width: 100%;
+    padding: 0;
+    // padding-right: 35px;
+    line-height: inherit;
     text-align: left;
     outline: 0 none;
     border: 0;
     text-decoration: none;
+    resize: none;
   }
 
   &-label {
     width: 80px;
     overflow: hidden;
-    display: inline-block;
+    margin-right: 6px;
     text-align: left;
-    display: flex;
-
     .label-string {
-      overflow: hidden;
-      white-space: nowrap;
-      text-overflow: ellipsis;
+      // overflow: hidden;
+      // white-space: nowrap;
+      // text-overflow: ellipsis;
     }
   }
-  .nut-textinput-clear {
+  &-value {
+    flex: 1;
+    vertical-align: middle;
+  }
+  &-inner {
+    display: flex;
+    align-items: center;
+  }
+  &-error-message {
+    color: $input-required-color;
+    font-size: 12px;
+  }
+  &-word-limit {
+    display: block;
+    margin-top: 4px;
+    color: #808080;
+    font-size: 12px;
+    // line-height: var(--van-field-word-limit-line-height);
+    text-align: right;
+  }
+  &-left-icon,
+  &-right-icon {
+    display: flex;
+    align-items: center;
+    font-size: 0;
+  }
+  &-clear,
+  &-right-icon {
+    margin-left: 4px;
+  }
+  &-left-icon {
+    margin-right: 4px;
+  }
+  &-clear {
     width: 16px;
     height: 16px;
-    position: absolute;
-    right: 15px;
+    color: #c8c9cc;
+    cursor: pointer;
+  }
+  &.nut-input-required {
+    &::before {
+      position: absolute;
+      left: 14px;
+      color: $input-required-color;
+      content: '*';
+    }
   }
   &-disabled {
     color: $input-disabled-color !important;
@@ -61,4 +99,9 @@
       -webkit-text-fill-color: $input-disabled-color;
     }
   }
+  &-error,
+  &-error::placeholder {
+    color: $input-required-color;
+    -webkit-text-fill-color: $input-required-color;
+  }
 }

+ 422 - 57
src/packages/__VUE/input/index.vue

@@ -1,46 +1,140 @@
 <template>
   <view :class="classes">
-    <view v-if="label" class="nut-input-label">
-      <view class="label-string">{{ label }}</view>
+    <view v-if="leftIcon && leftIcon.length > 0" class="nut-input-left-icon" @click="onClickLeftIcon">
+      <nut-icon :name="leftIcon" :size="leftIconSize"></nut-icon>
     </view>
-    <input
-      class="input-text"
-      :style="styles"
-      :type="type"
-      :maxlength="maxLength"
-      :placeholder="placeholder"
-      :disabled="disabled"
-      :readonly="readonly"
-      :value="modelValue"
-      :inputmode="inputmode"
-      @input="valueChange"
-      @focus="valueFocus"
-      @blur="valueBlur"
-    />
     <view
-      @click="handleClear"
-      class="nut-textinput-clear"
-      v-if="clearable && !readonly"
-      v-show="active && modelValue.length > 0"
+      v-if="label"
+      class="nut-input-label"
+      :class="labelClass"
+      :style="{
+        width: `${labelWidth}px`,
+        textAlign: labelAlign
+      }"
     >
-      <nut-icon name="close-little" size="12px"></nut-icon>
+      <view class="label-string">
+        {{ label }}
+        {{ colon ? ':' : '' }}
+      </view>
+    </view>
+    <view class="nut-input-value">
+      <view class="nut-input-inner" @click="onClickInput">
+        <textarea
+          v-if="type == 'textarea'"
+          class="input-text"
+          ref="inputRef"
+          :style="stylesTextarea"
+          :maxlength="maxLength"
+          :placeholder="placeholder"
+          :disabled="disabled"
+          :readonly="readonly"
+          :value="modelValue"
+          :formatTrigger="formatTrigger"
+          :autofocus="autofocus"
+          @input="onInput"
+          @focus="onFocus"
+          @blur="onBlur"
+        />
+        <input
+          v-else
+          class="input-text"
+          ref="inputRef"
+          :style="styles"
+          :type="inputType(type)"
+          :maxNum="maxNum"
+          :placeholder="placeholder"
+          :disabled="disabled"
+          :readonly="readonly"
+          :value="modelValue"
+          :formatTrigger="formatTrigger"
+          :autofocus="autofocus"
+          @input="onInput"
+          @focus="onFocus"
+          @blur="onBlur"
+        />
+        <nut-icon
+          class="nut-input-clear"
+          v-if="clearable && !readonly"
+          v-show="active && modelValue.length > 0"
+          :name="clearIcon"
+          :size="clearSize"
+          @click="clear"
+        >
+        </nut-icon>
+        <view v-if="rightIcon && rightIcon.length > 0" class="nut-input-right-icon" @click="onClickRightIcon">
+          <nut-icon :name="rightIcon" :size="rightIconSize"></nut-icon>
+        </view>
+        <slot v-if="$slots.button" name="button" class="nut-input-button"></slot>
+      </view>
+      <view v-if="showWordLimit && maxNum" class="nut-input-word-limit">
+        <span class="nut-input-word-num">{{ modelValue ? modelValue.length : 0 }}</span
+        >/{{ maxNum }}
+      </view>
+      <view
+        v-if="errorMessage"
+        class="nut-input-error-message"
+        :style="{
+          textAlign: errorMessageAlign
+        }"
+      >
+        {{ errorMessage }}
+      </view>
     </view>
   </view>
 </template>
 <script lang="ts">
-import { ref, computed } from 'vue';
+import { PropType, ref, reactive, computed, onMounted, watch, nextTick, inject } from 'vue';
 import { createComponent } from '../../utils/create';
 import { formatNumber } from './util';
 
 const { componentName, create } = createComponent('input');
 interface Events {
-  eventName: 'change' | 'focus' | 'blur' | 'clear' | 'update:modelValue';
+  eventName: 'focus' | 'blur' | 'clear' | 'change' | 'update:modelValue';
   params: (string | number | Event)[];
 }
+export type InputAlignType = 'left' | 'center' | 'right'; // text-align
+export type InputFormatTrigger = 'onChange' | 'onBlur'; // onChange: 在输入时执行格式化 ; onBlur: 在失焦时执行格式化
+export type InputType =
+  | 'tel'
+  | 'url'
+  | 'date'
+  | 'file'
+  | 'text'
+  | 'time'
+  | 'week'
+  | 'color'
+  | 'digit'
+  | 'email'
+  | 'image'
+  | 'month'
+  | 'radio'
+  | 'range'
+  | 'reset'
+  | 'button'
+  | 'hidden'
+  | 'number'
+  | 'search'
+  | 'submit'
+  | 'checkbox'
+  | 'password'
+  | 'textarea'
+  | 'datetime-local';
+
+export type InputRule = {
+  pattern?: RegExp;
+  message?: string;
+  required?: boolean;
+  validator?: FieldRuleValidator; // 通过函数进行校验
+};
+
 export default create({
   props: {
-    type: {
+    ref: {
       type: String,
+      default: ''
+    },
+    type: {
+      type: String as PropType<InputType>,
       default: 'text'
     },
     modelValue: {
@@ -55,7 +149,31 @@ export default create({
       type: String,
       default: ''
     },
-    requireShow: {
+    labelClass: {
+      type: String,
+      default: ''
+    },
+    labelWidth: {
+      type: [String, Number],
+      default: '80'
+    },
+    labelAlign: {
+      type: String as PropType<InputAlignType>,
+      default: 'left'
+    },
+    colon: {
+      type: Boolean,
+      default: false
+    },
+    inputAlign: {
+      type: String,
+      default: 'left'
+    },
+    center: {
+      type: Boolean,
+      default: false
+    },
+    required: {
       type: Boolean,
       default: false
     },
@@ -67,101 +185,348 @@ export default create({
       type: Boolean,
       default: false
     },
-    textAlign: {
+    error: {
+      type: Boolean,
+      default: false
+    },
+    maxNum: {
+      type: [String, Number],
+      default: ''
+    },
+    leftIcon: {
       type: String,
-      default: 'left'
+      default: ''
     },
-    maxLength: {
+    leftIconSize: {
+      type: [String, Number],
+      default: ''
+    },
+    rightIcon: {
+      type: String,
+      default: ''
+    },
+    rightIconSize: {
       type: [String, Number],
       default: ''
     },
     clearable: {
       type: Boolean,
+      default: false
+    },
+    clearIcon: {
+      type: String,
+      default: 'mask-close'
+    },
+    clearSize: {
+      type: [String, Number],
+      default: '14'
+    },
+    border: {
+      type: Boolean,
       default: true
     },
-    hasBorder: {
+    formatTrigger: {
+      type: String as PropType<InputFormatTrigger>,
+      default: 'onChange'
+    },
+    formatter: {
+      type: Function as PropType<(value: string) => string>,
+      default: null
+    },
+    rules: {
+      type: Array as PropType<InputRule>,
+      default: []
+    },
+    errorMessage: {
+      type: String,
+      default: ''
+    },
+    errorMessageAlign: {
+      type: String as PropType<InputAlignType>,
+      default: ''
+    },
+    rows: {
+      type: [String, Number],
+      default: null
+    },
+    showWordLimit: {
       type: Boolean,
       default: true
+    },
+    autofocus: {
+      type: Boolean,
+      default: false
     }
   },
 
-  emits: ['change', 'update:modelValue', 'blur', 'focus', 'clear'],
+  emits: [
+    'update:modelValue',
+    'change',
+    'blur',
+    'focus',
+    'clear',
+    'keypress',
+    'click-input',
+    'click-left-icon',
+    'click-right-icon'
+  ],
 
-  setup(props, { emit }) {
+  setup(props, { emit, slots }) {
     const active = ref(false);
 
+    const inputRef = ref<HTMLInputElement>();
+    const customValue = ref<() => unknown>();
+    const getModelValue = () => String(props.modelValue ?? '');
+    const form = inject('form');
+
+    const state = reactive({
+      focused: false,
+      validateFailed: false, // 校验失败
+      validateMessage: '' // 校验信息
+    });
+
     const classes = computed(() => {
       const prefixCls = componentName;
       return {
         [prefixCls]: true,
+        center: props.center,
         [`${prefixCls}-disabled`]: props.disabled,
-        [`${prefixCls}-require`]: props.requireShow,
-        [`${prefixCls}-border`]: props.hasBorder
+        [`${prefixCls}-required`]: props.required,
+        [`${prefixCls}-error`]: props.error,
+        [`${prefixCls}-border`]: props.border
       };
     });
 
-    const inputmode = computed(() => {
-      return props.type === 'digit' ? 'decimal' : props.type === 'number' ? 'numeric' : 'text';
-    });
-
     const styles = computed(() => {
       return {
-        textAlign: props.textAlign
+        textAlign: props.inputAlign
+      };
+    });
+    const stylesTextarea = computed(() => {
+      return {
+        textAlign: props.inputAlign,
+        height: Number(props.rows) * 24 + 'px'
       };
     });
 
-    const valueChange = (event: Event) => {
+    const inputType = (type: string) => {
+      if (type === 'number') {
+        return 'text';
+      } else if (type === 'digit') {
+        return 'tel';
+      } else {
+        return type;
+      }
+    };
+
+    const formValue = computed(() => {
+      if (customValue.value && slots.input) {
+        return customValue.value();
+      }
+      return props.modelValue;
+    });
+
+    // const inputmode = computed(() => {
+    //   return props.type === 'digit' ? 'decimal' : props.type === 'number' ? 'numeric' : 'text';
+    // });
+
+    const onInput = (event: Event) => {
       const input = event.target as HTMLInputElement;
-      let val = input.value;
+      let value = input.value;
+
+      // if (!event.target!.composing) {
+      //   updateValue((event.target as HTMLInputElement).value);
+      // }
+      updateValue(value);
+    };
 
+    const blur = () => inputRef.value?.blur();
+    const focus = () => inputRef.value?.focus();
+
+    const updateValue = (value: string, trigger: InputFormatTrigger = 'onChange') => {
       if (props.type === 'digit') {
-        val = formatNumber(val, true);
+        value = formatNumber(value, false, false);
       }
       if (props.type === 'number') {
-        val = formatNumber(val, false);
+        // console.log('value', value)
+        value = formatNumber(value, true, true);
+      }
+
+      if (props.formatter && trigger === props.formatTrigger) {
+        value = props.formatter(value);
       }
-      if (props.maxLength && val.length > Number(props.maxLength)) {
-        val = val.slice(0, Number(props.maxLength));
+
+      // if (props.maxNum && value.length > Number(props.maxNum)) {
+      //   value = value.slice(0, Number(props.maxNum));
+      // }
+
+      if (inputRef.value && inputRef.value.value !== value) {
+        inputRef.value.value = value;
+      }
+
+      if (value !== props.modelValue) {
+        emit('update:modelValue', value);
+        emit('change', value);
       }
-      emit('update:modelValue', val, event);
-      emit('change', val, event);
     };
 
-    const valueFocus = (event: Event) => {
+    // const limitValueLength = (value: string) => {
+    //   const { maxNum } = props;
+    //   if (isDef(maxNum) && getStringLength(value) > maxNum) {
+    //     const modelValue = getModelValue();
+    //     if (modelValue && getStringLength(modelValue) === +maxNum) {
+    //       return modelValue;
+    //     }
+    //     return cutString(value, +maxLength);
+    //   }
+    //   return value;
+    // };
+
+    const onFocus = (event: Event) => {
       const input = event.target as HTMLInputElement;
       let value = input.value;
       active.value = true;
       emit('focus', value, event);
+      // if (getProp('readonly')) {
+      //   blur();
+      // }
     };
 
-    const valueBlur = (event: Event) => {
+    const onBlur = (event: Event) => {
       setTimeout(() => {
         active.value = false;
       }, 200);
 
+      // if (getProp('readonly')) {
+      //   return;
+      // }
+
       const input = event.target as HTMLInputElement;
       let value = input.value;
-      if (props.maxLength && value.length > Number(props.maxLength)) {
-        value = value.slice(0, Number(props.maxLength));
+      if (props.maxNum && value.length > Number(props.maxNum)) {
+        value = value.slice(0, Number(props.maxNum));
       }
+      updateValue(getModelValue(), 'onBlur');
       emit('blur', value, event);
+      validateWithTrigger('onBlur');
     };
 
-    const handleClear = (event: Event) => {
+    const clear = (event: Event) => {
       emit('update:modelValue', '', event);
       emit('change', '', event);
-      emit('clear', '');
+      emit('clear', '', event);
+    };
+
+    // const runRules = (rules: InputRule[]) =>
+    //   rules.reduce(
+    //     (promise, rule) =>
+    //       promise.then(() => {
+    //         console.log('promise', promise, 'rule', rule)
+    //         if (state.validateFailed) {
+    //           return;
+    //         }
+
+    //         let { value } = formValue;
+
+    //         // if (rule.formatter) {
+    //         //   value = rule.formatter(value, rule);
+    //         // }
+
+    //         if (!runSyncRule(value, rule)) {
+    //           state.validateFailed = true;
+    //           state.validateMessage = getRuleMessage(value, rule);
+    //           return;
+    //         }
+
+    //         if (rule.validator) {
+    //           return runRuleValidator(value, rule).then((result) => {
+    //             if (result && typeof result === 'string') {
+    //               state.validateFailed = true;
+    //               state.validateMessage = result;
+    //             } else if (result === false) {
+    //               state.validateFailed = true;
+    //               state.validateMessage = getRuleMessage(value, rule);
+    //             }
+    //           });
+    //         }
+    //       }),
+    //     Promise.resolve()
+    //   );
+
+    const resetValidation = () => {
+      if (state.validateFailed) {
+        state.validateFailed = false;
+        state.validateMessage = '';
+      }
+    };
+
+    // const validate = (rules: any) =>
+    //   new Promise<void>((resolve) => {
+    //     console.log('rules122', form.props.rules)
+    //     resetValidation();
+    //     if (rules) {
+    //       console.log('rules233', rules)
+    //       runRules(rules).then(() => {
+    //         if (state.validateFailed) {
+    //           resolve({
+    //             // name: rules,
+    //             message: state.validateMessage
+    //           });
+    //         } else {
+    //           resolve();
+    //         }
+    //       });
+    //     } else {
+    //       resolve();
+    //     }
+    //   });
+
+    const validateWithTrigger = (trigger: InputFormatTrigger) => {
+      if (form && form.props.rules) {
+        const rules = form.props.rules;
+        if (rules) {
+          // console.log('rules', rules)
+          // validate(rules);
+        }
+      }
+    };
+
+    const onClickInput = (event: MouseEvent) => {
+      emit('click-input', event);
     };
 
+    const onClickLeftIcon = (event: MouseEvent) => emit('click-left-icon', event);
+
+    const onClickRightIcon = (event: MouseEvent) => emit('click-right-icon', event);
+
+    watch(
+      () => props.modelValue,
+      () => {
+        updateValue(getModelValue());
+        resetValidation();
+        validateWithTrigger('onChange');
+      }
+    );
+
+    onMounted(() => {
+      updateValue(getModelValue(), props.formatTrigger);
+    });
+
     return {
+      inputRef,
       active,
       classes,
       styles,
-      inputmode,
-      valueChange,
-      valueFocus,
-      valueBlur,
-      handleClear
+      stylesTextarea,
+      inputType,
+      // inputmode,
+      onInput,
+      onFocus,
+      onBlur,
+      clear,
+      onClickInput,
+      onClickLeftIcon,
+      onClickRightIcon
     };
   }
 });

+ 2 - 7
src/packages/__VUE/input/util.ts

@@ -12,15 +12,11 @@ function trimExtraChar(value: string, char: string, regExp: RegExp) {
   return value.slice(0, index + 1) + value.slice(index).replace(regExp, '');
 }
 
-export function formatNumber(
-  value: string,
-  allowDot = true,
-  allowMinus = true
-) {
+export function formatNumber(value: string, allowDot = true, allowMinus = true) {
   if (allowDot) {
     value = trimExtraChar(value, '.', /\./g);
   } else {
-    value = value.replace(/\./g, '');
+    value = value.split('.')[0];
   }
 
   if (allowMinus) {
@@ -28,7 +24,6 @@ export function formatNumber(
   } else {
     value = value.replace(/-/, '');
   }
-
   const regExp = allowDot ? /[^-0-9.]/g : /[^-0-9]/g;
 
   return value.replace(regExp, '');

+ 57 - 9
src/packages/__VUE/price/__tests__/price.spec.ts

@@ -12,23 +12,71 @@ afterAll(() => {
   config.global.components = {};
 });
 
-test('size props', () => {
+test('base price', () => {
   const wrapper = mount(Price, {
     props: {
-      size: 'small'
+      price: '199.99'
+    }
+  });
+  const price: any = wrapper.find('.nut-price');
+  console.log('price', price.text());
+  expect(price.text()).toBe('¥199.99');
+});
+
+// test('base price', () => {
+//   const wrapper = mount(Price, {
+//     props: {
+//       price: '199.999',
+//       decimalDigits: '2'
+//     }
+//   });
+//   const price: any = wrapper.find('.nut-price');
+//   console.log('price', price.text())
+//   expect(price.text()).toBe('¥199.99');
+// });
+
+test('decimalDigits price', () => {
+  const wrapper = mount(Price, {
+    props: {
+      price: '299.95',
+      decimalDigits: '1'
     }
   });
-  const avatar: any = wrapper.find('.nut-avatar');
-  expect(avatar.classes()).toContain('avatar-small');
-  expect(avatar.classes());
+  const price: any = wrapper.find('.nut-price');
+  console.log('price', price.text());
+  expect(price.text()).toBe('¥299.9');
 });
 
-test('shape props', () => {
+test('default needSymbol props', () => {
+  const wrapper = mount(Price);
+  const price: any = wrapper.find('.nut-price');
+  expect(price.find('.nut-price--symbol').text()).toBe('¥');
+});
+// test('needSymbol props', () => {
+//   const wrapper = mount(Price, {
+//     props: {
+//       needSymbol: false
+//     }
+//   });
+//   const price: any = wrapper.find('.nut-price');
+//   expect(price.find('.nut-price--symbol')).toBeNull();
+// });
+test('symbol props', () => {
   const wrapper = mount(Price, {
     props: {
-      bgColor: '#000000'
+      symbol: '$'
+    }
+  });
+  const price: any = wrapper.find('.nut-price');
+  expect(price.find('.nut-price--symbol').text()).toBe('$');
+});
+
+test('size props', () => {
+  const wrapper = mount(Price, {
+    props: {
+      size: 'small'
     }
   });
-  const avatar: any = wrapper.find('.nut-avatar');
-  expect(avatar.element.style.backgroundColor).toBe('rgb(0, 0, 0)');
+  const price: any = wrapper.find('.nut-price');
+  expect(price.html()).toContain('nut-price--small');
 });

+ 1 - 1
src/packages/styles/variables.scss

@@ -153,7 +153,7 @@ $picker-item-active-line-border: 1px solid #d8d8d8 !default;
 //input
 $input-border-bottom: #eaf0fb !default;
 $input-disabled-color: #c8c9cc !default;
-$input-require-color: $primary-color !default;
+$input-required-color: $primary-color !default;
 $input-font-size: $font-size-2 !default;
 
 // textarea