ソースを参照

feat: inputnumber component

suzigang 5 年 前
コミット
0e8728b996

+ 14 - 0
src/config.ts

@@ -207,6 +207,20 @@ export const nav = [
     ]
   },
   {
+    name: '数据录入',
+    packages: [
+      {
+        name: 'InputNumber',
+        sort: 1,
+        cName: '数字输入框',
+        type: 'component',
+        show: true,
+        desc: '数字输入框组件',
+        author: 'szg2008'
+      }
+    ]
+  },
+  {
     name: '业务组件',
     packages: []
   }

+ 106 - 0
src/packages/inputnumber/demo.vue

@@ -0,0 +1,106 @@
+<template>
+  <div class="demo">
+    <h2>基本用法</h2>
+    <nut-cell>
+      <nut-inputnumber
+        v-model:modelValue="state.val1"
+        @change="change"
+        @blur="blur"
+        @focus="focus"
+      />
+    </nut-cell>
+    <h2>步长设置</h2>
+    <nut-cell>
+      <nut-inputnumber
+        v-model:modelValue="state.val2"
+        :step="state.step"
+        :decimalPlaces="1"
+      />
+    </nut-cell>
+    <h2>限制输入范围</h2>
+    <nut-cell>
+      <nut-inputnumber
+        v-model:modelValue="state.val3"
+        :min="state.min"
+        :max="state.max"
+        @reduce-no-allow="reduceNoAllow"
+        @add-no-allow="addNoAllow"
+      />
+    </nut-cell>
+    <h2>禁用操作&输入框</h2>
+    <nut-cell>
+      <nut-inputnumber
+        :readonly="true"
+        v-model:modelValue="state.val4"
+        min="0"
+        max="0"
+        @focus="focus"
+        @blur="blur"
+      />
+    </nut-cell>
+    <h2>支持异步修改(点击+/-,手动改成了3)</h2>
+    <nut-cell>
+      <nut-inputnumber
+        :async="state.async"
+        @change="handleChangeAsync"
+        v-model:modelValue="state.val5"
+      />
+    </nut-cell>
+  </div>
+</template>
+
+<script lang="ts">
+import { reactive, onMounted } from 'vue';
+import { createComponent } from '@/utils/create';
+const { createDemo } = createComponent('inputnumber');
+export default createDemo({
+  props: {},
+  setup() {
+    const state = reactive({
+      val1: 2,
+      val2: 1.1,
+      val3: 3,
+      val4: 0,
+      val5: 1,
+      step: 1.1,
+      min: 3,
+      max: 100,
+      async: true
+    });
+    onMounted(() => {
+      state.max = 5;
+    });
+    const change = (num: string | number) => {
+      console.log('change: ', num);
+    };
+    const blur = (e: Event, num: string | number) => {
+      console.log('blur: ', num);
+    };
+    const focus = (e: Event, num: string | number) => {
+      console.log('focus: ', e, num);
+    };
+    const addNoAllow = () => {
+      alert('超出最大限制数');
+    };
+    const reduceNoAllow = () => {
+      alert('超出最小限制数');
+    };
+    const handleChangeAsync = (num: number) => {
+      setTimeout(() => {
+        state.val5 = 3;
+      }, 1000);
+    };
+    return {
+      state,
+      change,
+      blur,
+      focus,
+      reduceNoAllow,
+      addNoAllow,
+      handleChangeAsync
+    };
+  }
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 94 - 0
src/packages/inputnumber/doc.md

@@ -0,0 +1,94 @@
+# InputNumber 数字输入框组件
+
+### 介绍
+
+基于
+
+### 安装
+
+``` javascript
+import { createApp } from 'vue';
+import { inputnumber } from '@nutui/nutui';
+
+const app = createApp();
+app.use(inputnumber);
+
+```
+
+## 代码演示
+
+### 基础用法1
+
+初始化一个默认值
+
+```html
+<nut-inputnumber v-model:modelValue="1" />
+```
+
+### 基础用法2
+
+设置步长`step` 和 保留的小数位`decimalPlaces`
+
+```html
+<nut-inputnumber v-model:modelValue="1" :step="1.1" :decimalPlaces="1" />
+```
+
+### 基础用法3
+
+`min` 和 `max` 属性分别表示最小值和最大值
+
+```html
+<nut-inputnumber v-model:modelValue="1" :min="3" :max="5" />
+```
+
+### 基础用法4
+
+`readonly`设置只读
+
+```html
+<nut-inputnumber v-model:modelValue="1" :readonly="true" />
+```
+
+### 基础用法5
+
+`size`设置操作符的大小
+
+```html
+<nut-inputnumber v-model:modelValue="1" :size="20px" />
+```
+
+### 高级用法
+
+`async`支持异步修改数量,设置了此属性为true,必须同时在`change`事件中手动设置input值才能生效
+
+```html
+<nut-inputnumber v-model:modelValue="1" :async="true" @change="change"/>
+```
+
+
+## API
+
+### Props
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| size         | 操作符+、-尺寸               | String          | 20px                |
+| color        | 操作符+、-颜色               | String          | #1a1a1a             |
+| disColor     | 操作符+、-禁用时颜色          | String          | #ccc                |
+| min          | 最小值                      | String、Number | 1                   |
+| max          | 最大值                      | String、Number | Infinity             |
+| step         | 步长                        | String、Number |     1                |
+| readonly     | 只读                   | Boolean | false              |
+| modelValue   | 初始值                   | String、Number | ''              |
+| decimalPlaces| 设置保留的小数位                   | String、Number | 1              |
+| async        | 支持异步                   | Boolean | false              |
+
+### Events
+
+| 事件名 | 说明           | 回调参数     |
+|--------|----------------|--------------|
+| change  | 值改变时触发 | num: string | number |
+| focus  | 输入框获取焦点时触发 | event: Event, num: string | number |
+| blur  | 输入框失去焦点时触发 | event: Event, num: string | number |
+| add-no-allow  | 超出最大事件回调 | - |
+| reduce-no-allow  | 超出最小事件回调 | - |

+ 21 - 0
src/packages/inputnumber/index.scss

@@ -0,0 +1,21 @@
+.nut-inputnumber {
+  display: flex;
+  align-items: center;
+  input {
+    width: $inputnumber-input-width;
+    height: $inputnumber-input-height;
+    text-align: center;
+    outline: none;
+    border: 0;
+    font-family: initial;
+    background-color: $inputnumber-input-background-color;
+    border-radius: $inputnumber-input-border-radius;
+    &:read-only {
+      color: $disable-color;
+    }
+  }
+  input::-webkit-outer-spin-button,
+  input::-webkit-inner-spin-button {
+    appearance: none;
+  }
+}

+ 322 - 0
src/packages/inputnumber/index.vue

@@ -0,0 +1,322 @@
+<template>
+  <view :class="classes">
+    <nut-icon
+      name="minus"
+      :size="size"
+      :color="getIconColor('minus')"
+      @click="reduce"
+    ></nut-icon>
+    <input
+      type="number"
+      :min="state.minVal"
+      :max="max"
+      :readonly="readonly"
+      :value="state.num"
+      @input="numChange"
+      @blur="blur"
+      @focus="focus"
+    />
+    <nut-icon
+      name="plus"
+      :size="size"
+      :color="getIconColor('plus')"
+      @click="add"
+    ></nut-icon>
+  </view>
+</template>
+<script lang="ts">
+import { computed, reactive, watch, toRefs } from 'vue';
+import { createComponent } from '@/utils/create';
+const { componentName, create } = createComponent('inputnumber');
+
+export default create({
+  props: {
+    size: {
+      type: [String],
+      default: '20px'
+    },
+    color: {
+      type: String,
+      default: '#1a1a1a'
+    },
+    disColor: {
+      type: String,
+      default: '#ccc'
+    },
+    min: {
+      type: [Number, String],
+      default: 0
+    },
+    max: {
+      type: [Number, String],
+      default: Infinity
+    },
+    step: {
+      type: [Number, String],
+      default: 1
+    },
+    readonly: {
+      type: Boolean,
+      default: false
+    },
+    modelValue: {
+      type: [String, Number],
+      default: ''
+    },
+    decimalPlaces: {
+      type: [String, Number],
+      default: 0
+    },
+    async: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: [
+    'update:modelValue',
+    'change',
+    'blur',
+    'focus',
+    'reduce',
+    'reduce-no-allow',
+    'add',
+    'add-no-allow'
+  ],
+
+  setup(props, { emit }) {
+    interface Events {
+      eventName:
+        | 'update:modelValue'
+        | 'change'
+        | 'focus'
+        | 'blur'
+        | 'add-no-allow'
+        | 'reduce-no-allow';
+      params: (string | number | Event)[];
+    }
+    const { modelValue, min, max, step } = toRefs(props);
+    const state = reactive({
+      num: !modelValue.value ? min.value : modelValue.value,
+      minVal: min.value,
+      tempVal: '',
+      focusing: false
+    });
+    const format = (v: string | number): string | number => {
+      if (v > max.value) {
+        v = max.value;
+      }
+      if (v < state.minVal) {
+        v = state.minVal;
+      }
+
+      return v;
+    };
+
+    const fixedDecimalPlaces = (v: string | number): string => {
+      return Number(v).toFixed(Number(props.decimalPlaces));
+    };
+
+    const getIconColor = (type: 'minus' | 'plus') => {
+      if (type === 'minus') {
+        return (state.focusing ? Number(state.tempVal) : Number(state.num)) -
+          Number(step.value) <
+          min.value
+          ? props.disColor
+          : props.color;
+      } else if (type === 'plus') {
+        return Number(state.num) > Number(max.value) - Number(step.value)
+          ? props.disColor
+          : props.color;
+      } else {
+        throw new Error('type is not be supported~');
+      }
+    };
+
+    const emitChange = (envs: Events[]) => {
+      envs.forEach(item => {
+        emit(item.eventName, ...item.params);
+      });
+    };
+
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const numChange = (e: Event) => {
+      const input = e.target as HTMLInputElement;
+      let val = input.value;
+      val = String(format(val));
+      input.value = val;
+      state.num = val;
+      emitChange([
+        {
+          eventName: 'update:modelValue',
+          params: [state.num]
+        },
+        {
+          eventName: 'change',
+          params: [state.num]
+        }
+      ]);
+    };
+
+    const focus = (e: Event) => {
+      if (props.readonly) return;
+      const val = String(state.num);
+      state.tempVal = val;
+      state.minVal = '';
+      state.focusing = true;
+      emitChange([
+        {
+          eventName: 'focus',
+          params: [e, state.num]
+        }
+      ]);
+    };
+
+    const blur = (e: Event) => {
+      if (props.async) {
+        emitChange([
+          {
+            eventName: 'change',
+            params: ['']
+          }
+        ]);
+        return;
+      }
+      const val = (e.target as HTMLInputElement).value;
+      state.minVal = String(min.value);
+      state.num = val ? format(val) : state.tempVal;
+      state.focusing = false;
+      emitChange([
+        {
+          eventName: 'update:modelValue',
+          params: [state.num]
+        },
+        {
+          eventName: 'blur',
+          params: [e, state.num]
+        }
+      ]);
+    };
+
+    const reduce = () => {
+      if (props.async) {
+        emitChange([
+          {
+            eventName: 'change',
+            params: [state.num]
+          }
+        ]);
+        return;
+      }
+      if (getIconColor('minus') === props.color) {
+        const [n1, n2] = fixedDecimalPlaces(
+          Number(state.num) - Number(props.step)
+        ).split('.');
+        const fixedLen = n2 ? n2.length : 0;
+        state.num = parseFloat(n1 + (n2 ? `.${n2}` : '')).toFixed(fixedLen);
+        emitChange([
+          {
+            eventName: 'update:modelValue',
+            params: [state.num]
+          },
+          {
+            eventName: 'change',
+            params: [state.num]
+          }
+        ]);
+      } else {
+        emitChange([
+          {
+            eventName: 'reduce-no-allow',
+            params: []
+          }
+        ]);
+      }
+    };
+
+    const add = () => {
+      if (props.async) {
+        emitChange([
+          {
+            eventName: 'change',
+            params: [state.num]
+          }
+        ]);
+        return;
+      }
+      if (getIconColor('plus') === props.color) {
+        const [n1, n2] = fixedDecimalPlaces(
+          Number(state.num) + Number(props.step)
+        ).split('.');
+        const fixedLen = n2 ? n2.length : 0;
+        state.num = parseFloat(n1 + (n2 ? '.' + n2 : '')).toFixed(fixedLen);
+        emitChange([
+          {
+            eventName: 'update:modelValue',
+            params: [state.num]
+          },
+          {
+            eventName: 'change',
+            params: [state.num]
+          }
+        ]);
+      } else {
+        emitChange([
+          {
+            eventName: 'add-no-allow',
+            params: []
+          }
+        ]);
+      }
+    };
+
+    watch(
+      () => min.value,
+      newValues => {
+        if (newValues < Number(max.value)) {
+          state.minVal = newValues;
+          const val = format(state.num);
+          state.num = val > 0 ? fixedDecimalPlaces(val) : val;
+        }
+      }
+    );
+
+    watch(
+      () => modelValue.value,
+      newValues => {
+        const val = format(newValues);
+        state.num = val > 0 ? fixedDecimalPlaces(val) : val;
+        emitChange([
+          {
+            eventName: 'change',
+            params: [state.num]
+          }
+        ]);
+      }
+    );
+
+    return {
+      state,
+      classes,
+      format,
+      getIconColor,
+      fixedDecimalPlaces,
+      emitChange,
+      numChange,
+      blur,
+      focus,
+      reduce,
+      add
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 16 - 4
src/packages/navbar/index.vue

@@ -6,15 +6,27 @@
         <nut-icon v-if="leftShow" color="#979797" name="left"></nut-icon>
       </view>
 
-      <view class="nut-navbar__title" :class="{ icon: icon }" v-if="title || titIcon">
+      <view
+        class="nut-navbar__title"
+        :class="{ icon: icon }"
+        v-if="title || titIcon"
+      >
         <view v-if="title">{{ title }}</view>
         <nut-icon v-if="titIcon" class="icon" :name="titIcon"></nut-icon>
       </view>
 
       <!-- 右侧  title/icon/多个tit/多个icon-->
-      <view class="nut-navbar__right" :class="{ icon: icon }" v-if="desc || icon">
-        <view v-if="desc" :style="{ 'text-align': descTextAlign }">{{ desc }}</view>
-        <view> <nut-icon v-if="icon" class="icon" :name="icon"></nut-icon></view>
+      <view
+        class="nut-navbar__right"
+        :class="{ icon: icon }"
+        v-if="desc || icon"
+      >
+        <view v-if="desc" :style="{ 'text-align': descTextAlign }">{{
+          desc
+        }}</view>
+        <view>
+          <nut-icon v-if="icon" class="icon" :name="icon"></nut-icon
+        ></view>
       </view>
     </slot>
   </view>

+ 12 - 1
src/styles/variables.scss

@@ -53,7 +53,11 @@ $button-primary-background-color: linear-gradient(
 );
 $button-info-color: $white;
 $button-info-border-color: rgba(73, 106, 242, 1);
-$button-info-background-color: linear-gradient(315deg, rgba(73, 143, 242, 1) 0%, rgba(73, 101, 242, 1) 100%);
+$button-info-background-color: linear-gradient(
+  315deg,
+  rgba(73, 143, 242, 1) 0%,
+  rgba(73, 101, 242, 1) 100%
+);
 $button-success-color: $white;
 $button-success-border-color: rgba(38, 191, 38, 1);
 $button-success-background-color: linear-gradient(
@@ -98,3 +102,10 @@ $icon-line-height: 20px;
 $uploader-width: 100px;
 $uploader-height: 100px;
 $uploader-babackground: #f7f8fa;
+
+// inputnumber
+
+$inputnumber-input-background-color: $help-color;
+$inputnumber-input-border-radius: 8px;
+$inputnumber-input-width: 40px;
+$inputnumber-input-height: 20px;