ソースを参照

添加数字输入框控件。

YangZhiJie 2 年 前
コミット
950c8bb82b

+ 1 - 1
examples/App.vue

@@ -4,7 +4,7 @@
       <div class="col-md-2">
         <ul class="nav nav-pills nav-stacked">
           <li>
-            <router-link :to="{ path: '/desktop/check-box-example'}">复选框</router-link>
+            <router-link :to="{ path: '/desktop/input-number-example'}">数字输入</router-link>
           </li>
           <li>
             <router-link :to="{ path: '/desktop/date-example'}">日期控件</router-link>

+ 129 - 0
examples/input-number/src/InputNumberExample.vue

@@ -0,0 +1,129 @@
+<template>
+  <h1>加载中</h1>
+  
+  <h2>没有任何绑定</h2>
+  <InputNumber />
+
+
+  <h2>正常值</h2>
+  <InputNumber v-model="modelValue1" />
+  <span>{{ modelValue1 }}</span>
+
+  <h2>只读</h2>
+  <InputNumber v-model="modelValue2" :readonly="readonly2" />
+  <span>{{ modelValue2 }}</span>
+
+  <h3>{{ title4 }}</h3>
+  <InputNumber v-model="modelValue4" :pattern="pattern4" />
+  <span>{{ modelValue4 }}</span>
+
+  <h3>{{ title5 }}</h3>
+  <InputNumber v-model="modelValue5" :pattern="pattern5" />
+  <span>{{ modelValue5 }}</span>
+
+  <h3>{{ title6 }}</h3>
+  <InputNumber v-model="modelValue6" :pattern="pattern6" />
+  <span>{{ modelValue6 }}</span>
+  
+  <h3>{{ title7 }}</h3>
+  <InputNumber v-model="modelValue7" :pattern="pattern7" />
+  <span>{{ modelValue7 }}</span>
+  
+  <h3>{{ title8 }}</h3>
+  <InputNumber v-model="modelValue8" :pattern="pattern8" />
+  <span>{{ modelValue8 }}</span>
+  
+  <h3>{{ title9 }}</h3>
+  <InputNumber v-model="modelValue9" :pattern="pattern9" />
+  <span>{{ modelValue9 }}</span>
+</template>
+
+<script>
+
+import InputNumber from '@/input-number/index.js';
+
+export default {
+
+  components: {
+    InputNumber,
+  },
+  data: function () {
+    return {
+      modelValue1: null,
+      modelValue2: null,
+      readonly2: true,
+      modelValue3: null,
+
+      title4: '语言环境:ja-JP,样式: 货币,币种: JPY',
+      pattern4: {
+        'local': 'ja-JP', 
+        options: { style: 'currency', currency: 'JPY'},
+      },
+      modelValue4: null,
+
+      title5: '语言环境:en-US,样式: 货币,币种: RMB',
+      modelValue5: null,
+      pattern5: {
+        'local': 'en-IN', 
+        options: { style: 'currency', currency: 'RMB' },
+      },
+
+      title6: '语言环境:en-US,样式: 货币,币种: USD',
+      modelValue6: null,
+      pattern6: {
+        'local': 'en-US', 
+        options: { 
+          style: 'currency',
+          currency: 'USD', 
+        },
+      },
+
+      title7: '语言环境:en-US,数的小数部分所允许的最小位数 2,数的小数部分所允许的最大位数 2',
+      modelValue7: null,
+      pattern7: {
+        'local': 'en-US', 
+        options: { 
+          minimumFractionDigits: 2,
+          maximumFractionDigits: 2,
+        },
+      },
+
+      title8: '语言环境:en-US,数的整数部分所允许的最小位数 4,数的小数部分所允许的最大位数 0',
+      modelValue8: null,
+      pattern8: {
+        'local': 'en-US', 
+        options: { 
+          minimumIntegerDigits: 4,
+          maximumFractionDigits: 0,
+        },
+      },
+
+
+      title9: '语言环境:en-US,样式:百分比,最小小数位数为 2,最大小数位数为 2',
+      modelValue9: null,
+      pattern9: {
+        'local': 'en-US', 
+        options: { 
+          style: 'percent',
+          minimumFractionDigits: 2,
+          maximumFractionDigits: 2,
+        },
+      },
+
+    };
+  },
+
+  methods: {
+    start: function(){
+      this.loading = true;
+    },
+    startAndStop: function(){
+      this.loading = true;
+      let _self = this;
+      window.setTimeout(function(){
+        _self.loading = false;
+      }, 5000);
+    },
+  },
+};
+</script>

+ 5 - 1
examples/route/index.js

@@ -15,6 +15,7 @@ const UploadWidgetExample = () => import(/* webpackChunkName: "tree-example" */
 const VueBootstrapPaginationExample = () => import(/* webpackChunkName: "tree-example" */ '../vue-bootstrap-pagination/src/VueBootstrapPaginationExample.vue');
 const VueMonthlyPickerExample = () => import(/* webpackChunkName: "vue-monthly-picker-example" */ '../vue-monthly-picker/src/VueMonthlyPickerExample.vue');
 const YearPickerExample = () => import(/* webpackChunkName: "year-picker-example" */ '../year-picker/src/YearPickerExample.vue');
+const InputNumberExample = () => import(/* webpackChunkName: "input-number-example" */ '../input-number/src/InputNumberExample.vue');
 
 
 
@@ -89,7 +90,10 @@ export default {
         /** 年份控件 */
         { path: 'year-picker-example', component: YearPickerExample },
 
-				
+        /** 数字输入框 */
+        { path: 'input-number-example', component: InputNumberExample },
+
+
         /** 搜索框 */
         { path: 'search-widget-example', component: SearchWidgetExample },
 

+ 3 - 1
package.json

@@ -6,7 +6,8 @@
   "scripts": {
     "dev": "cross-env webpack serve --open --hot --config webpack.dev.js",
     "build": "cross-env NODE_ENV=production webpack --mode=production --config webpack.prod.js --progress",
-    "lib": "cross-env NODE_ENV=production webpack --progress --config webpack.lib.js"
+    "lib": "cross-env NODE_ENV=production webpack --progress --config webpack.lib.js",
+    "test": "jest"
   },
   "directories": {
     "dist": "dist"
@@ -29,6 +30,7 @@
     "eslint-webpack-plugin": "^3.1.1",
     "file-loader": "^6.2.0",
     "html-webpack-plugin": "^5.5.0",
+    "jest": "^29.7.0",
     "mini-css-extract-plugin": "^2.6.0",
     "style-loader": "^3.3.1",
     "terser-webpack-plugin": "^5.3.6",

+ 8 - 0
packages/input-number/index.js

@@ -0,0 +1,8 @@
+
+import InputNumber from './src/InputNumber.vue';
+
+InputNumber.install = function(Vue) {
+  Vue.component(InputNumber.name, InputNumber);
+};
+
+export default InputNumber;

+ 124 - 0
packages/input-number/src/InputNumber.vue

@@ -0,0 +1,124 @@
+<template>
+  <input
+    :value="displayValue"
+    autocomplete="off"
+    class="form-control"
+    :readonly="readonly"
+    :disabled="readonly"
+    onmousewheel="return false;"
+    @mousewheel="mouseWheelEvent"
+    @keyup="change"
+  />
+</template>
+
+<script>
+import { formatNumber, unformatNumber } from '../src/NumberTransform.js';
+
+export default {
+  name: 'InputNumber',
+
+  components: {},
+
+  props: {
+    modelValue: {
+      type: Number,
+      default: null,
+    },
+    readonly: {
+      type: Boolean,
+      default: false,
+    },
+    pattern: {
+      type: Object,
+      default: function(){
+        return {};
+      },
+    },
+  },
+
+  emits: ['update:modelValue'],
+
+  data: function () {
+    return {
+      isValid: true,
+      displayValue: null,
+    //   pattern: {
+    //     locale: 'zh-CN',
+    //     options: {
+    //         style: 'currency', 
+    //         currency: 'JPY'
+    //     }
+    //   }
+    };
+  },
+
+  watch: {
+    modelValue: {
+      handler: function(newValue, oldValue){
+        this.computeDisplayValue(newValue);
+      },
+      immediate: true,
+    },
+    pattern: {
+      handler: function(newValue, oldValue){
+        this.computeDisplayValue(this.modelValue);
+      },
+      immediate: true,
+    },
+  },
+
+  
+
+  mounted: function () {},
+
+  methods: {
+    /**
+     * 当输入的值发生改变
+     */
+    change: function (event) {
+      let _self = this;
+      let inputValue = event.target.value;
+      if (
+        _self.pattern != null
+      ) {
+        // 根据输入的值,获取实际的值
+        let newValue = unformatNumber(inputValue, _self.pattern.locale, _self.pattern.options);
+        let newValueString = formatNumber(newValue, _self.pattern.locale, _self.pattern.options);
+        newValue = unformatNumber(newValueString, _self.pattern.locale, _self.pattern.options);
+        // 根据实际的值,计算新的格式
+        // _self.displayValue = newValueString;
+        // event.target.value = newValueString;
+        _self.$emit('update:modelValue', newValue);
+      }
+    },
+
+    /**
+     * 计算显示的值
+     */
+    computeDisplayValue: function(value){
+      let _self = this;
+      if (
+        _self.pattern != null
+      ) {
+        // 根据实际的值,计算新的格式
+        _self.displayValue = formatNumber(value, _self.pattern.locale, _self.pattern.options);
+      }else{
+        _self.displayValue = formatNumber(value);
+      }
+    },
+    
+    /**
+     * 禁止滚轮滚动影响数字
+     */
+    mouseWheelEvent: function (event) {
+      if (event) {
+        event.preventDefault();
+      }
+    },
+
+  },
+};
+</script>
+
+<style scoped>
+</style>

+ 100 - 0
packages/input-number/src/NumberTransform.js

@@ -0,0 +1,100 @@
+/**
+ * 数字转换工具类
+ */
+
+/**
+ * 数字转换 成 国际化数字字符串
+ * @param number 待转换的数字
+ * @param locales 用于格式化数字的语言环境。默认值为当前用户的语言环境。
+ *      可以是语言标识符号(BCP 47 language tag) 举例: 'de-DE', 'ja-JP', 'en-IN'
+ *      还可以是 Intl.Locale 的实例。
+ * @param options 
+ *      options.style 格式化的样式,可以是如下的选项:
+ *          "decimal" (默认) 存数字类型(十进制)
+ *          "currency" 货币类型
+ *          "percent"  百分比类型
+ *          "unit"  单位类型
+ *      options.currency 货币类型,其值是ISO 4217 货币编码。
+ *          "USD" 美元
+ *          "EUR" 德元
+ *          "CNY" 人名币
+ *      options.currency 货币类型,其值是ISO 4217 货币编码。
+ *      options.currencyDisplay 如何显示货币样式。
+ *          "code" ISO货币编码
+ *          "symbol" 货币符号,举例:€.
+ *          "narrowSymbol" 窄格式符号(“$100”而不是“US$100”)。
+ *          "name" 货币名称,举例:dollar
+ *      options.useGrouping 是否使用分组分隔符,如千位分隔符或千/万/亿分隔符.可能的值是true和false,默认值是true.
+ *      options.minimumIntegerDigits:数的整数部分所允许的最小位数,如果位数不够,则在数字前用0补齐。默认值为 1。
+ *          举例:1.545 -> minimumIntegerDigits: 2 -> 01.545
+ *      options.minimumFractionDigits:数的小数部分所允许的最小位数,如果位数不够,则在数字后用0补齐。默认值为 0。(百分比)
+ *          举例:1.545 -> minimumFractionDigits: 4 -> 1.5450
+ *      options.maximumFractionDigits:数的小数部分所允许的最大位数。默认值为 3。(百分比)
+ *          举例:1.545 -> maximumFractionDigits: 2 -> 1.55
+ *      options.minimumSignificantDigits:数字所允许的最小有效数字位数。默认值为 1。
+ *          举例:1.545 -> minimumSignificantDigits: 5 -> 1.5450
+ *      options.maximumSignificantDigits:数字所允许的最大有效数字位数。默认值为 21。
+ *          举例:1.545 -> maximumSignificantDigits: 3 -> 1.55
+ * 
+ */
+const formatNumber = function(number, locales, options){
+  if(number == null || number == ''){
+    return null;
+  }
+  return new Intl.NumberFormat(locales, options).format(number);
+};
+
+/**
+ * Parse a localized number to a float.
+ * 把国际化数字字符串 转换成 数字
+ * @param {string} stringNumber - the localized number. 国际化数字字符串
+ * @param {string} locale - [可选] the locale that the number is represented in. Omit this parameter to use the current locale.
+ 
+ */
+const unformatNumber = function(stringNumber, locale, options){
+  // var thousandSeparator = Intl.NumberFormat(locale).format(11111).replace(/\p{Number}/gu, '');
+  // var decimalSeparator = Intl.NumberFormat(locale).format(1.1).replace(/\p{Number}/gu, '');
+  // console.log(thousandSeparator, decimalSeparator);
+  // return parseFloat(stringNumber
+  //     .replace(new RegExp('\\' + thousandSeparator, 'g'), '')
+  //     .replace(new RegExp('\\' + decimalSeparator), '.')
+  // );
+  if(stringNumber == null){
+    return null;
+  }
+
+  // 千分位分隔符
+  const thousandSeparator = Intl.NumberFormat(locale).formatToParts(11111)[1].value;
+  // 小数位分隔符
+  const decimalSeparator = Intl.NumberFormat(locale).formatToParts(1.1)[1].value;
+  // 货币
+  let currency = Intl.NumberFormat(locale, options).formatToParts(1.1).find(x => x.type === 'currency');
+  if(currency != null){
+    currency = currency.value;
+  }
+
+  //console.log(thousandSeparator, decimalSeparator, currency);
+
+  let reversedVal = stringNumber.replace(new RegExp('\\' + thousandSeparator, 'g'), '');
+
+  reversedVal = reversedVal.replace(new RegExp('\\' + decimalSeparator, 'g'), '.');
+        
+  if(currency != null){
+    // 替换货币
+    reversedVal = reversedVal.replace(new RegExp('\\' + currency, 'g'), '');
+  }
+
+  if(reversedVal.indexOf('%') >= 0){
+    // 如果是百分数
+    return Number(reversedVal.replace('%', '')) / 100;
+  }
+
+  console.log(reversedVal);
+
+  return Number.isNaN(parseFloat(reversedVal)) ? null : Number(reversedVal);
+};
+
+export {
+  formatNumber,
+  unformatNumber,
+};

+ 65 - 0
test/input-number/NumberTransform.test.js

@@ -0,0 +1,65 @@
+import {formatNumber, unformatNumber} from '../../packages/input-number/src/NumberTransform.js';
+
+
+test('number formate test', () => {
+    var number = 123456.789;
+    //let value1 = formatNumber('de-DE', { style: 'currency', currency: 'EUR' }, number);
+    // console.log(value1);
+    //expect(value1).toEqual("123.456,79 €");
+    
+    let value2 = formatNumber(number, 'ja-JP', { style: 'currency', currency: 'JPY' });
+    expect(value2).toEqual("¥123,457");
+    let numberValue2 = unformatNumber(value2, 'ja-JP', { style: 'currency', currency: 'JPY' });
+    expect(numberValue2).toEqual(123457);
+    
+    let value3 = formatNumber(number, 'en-IN', { maximumSignificantDigits: 3 });
+    expect(value3).toEqual("1,23,000");
+    let numberValue3 = unformatNumber(value3, 'en-IN', { maximumSignificantDigits: 3 });
+    expect(numberValue3).toEqual(123000);
+
+    number = 1234.56;
+    // 将格式化数字为美元货币格式
+    let value4 = formatNumber(number, 'en-US', { style: 'currency',
+        currency: 'USD' });
+    expect(value4).toEqual("$1,234.56");
+    
+    let numberValue4 = unformatNumber(value4, 'en-US', { style: 'currency',
+        currency: 'USD' });
+    expect(numberValue4).toEqual(1234.56);
+
+    // 使用了en-US作为语言环境,并将最小小数位数和最大小数位数都设置为 2
+    let value5 = formatNumber(number, 'en-US', { 
+        minimumFractionDigits: 2,
+        maximumFractionDigits: 2
+    });
+    expect(value5).toEqual("1,234.56");
+    let numberValue5 = unformatNumber(value5, 'en-US', {  minimumFractionDigits: 2,
+        maximumFractionDigits: 2 });
+    expect(numberValue5).toEqual(1234.56);
+
+    // 使用了en-US作为语言环境,并将最小整数位数设置为 4
+    number = 12345.6789
+    let value6 = formatNumber(number, 'en-US', { 
+        minimumIntegerDigits: 4,
+        maximumFractionDigits: 0
+    });
+    expect(value6).toEqual("12,346");
+    let numberValue6 = unformatNumber(value6, 'en-US', { 
+        minimumIntegerDigits: 4,
+        maximumFractionDigits: 0 });
+    expect(numberValue6).toEqual(12346);
+
+    // 使用了en-US作为语言环境,并将样式设置为percent。我们还指定了最小小数位数为 2,最大小数位数为 2。
+    number = 0.75;
+    let value7 = formatNumber(number, 'en-US', { 
+        style: 'percent',
+        minimumFractionDigits: 2,
+        maximumFractionDigits: 2,
+    });
+    expect(value7).toEqual("75.00%");
+    let numberValue7 = unformatNumber(value7, 'en-US', { 
+        style: 'percent',
+        minimumFractionDigits: 2,
+        maximumFractionDigits: 2, });
+    expect(numberValue7).toEqual(0.75);
+});