n20-common-lib 3.3.14 → 3.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n20-common-lib",
3
- "version": "3.3.14",
3
+ "version": "3.3.16",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -1,8 +1,8 @@
1
1
  <template>
2
- <el-descriptions class="n20-descriptions" :column="column" size="default" border>
2
+ <el-descriptions class="n20-descriptions" :column="currentColumn" size="default" border>
3
3
  <slot></slot>
4
- <template v-if="this.$slots.default.length === 1">
5
- <el-descriptions-item v-for="(item, index) in column - 1" :key="index" />
4
+ <template v-if="this.$slots.default && this.$slots.default.length === 1">
5
+ <el-descriptions-item v-for="(item, index) in currentColumn - 1" :key="index" />
6
6
  </template>
7
7
  </el-descriptions>
8
8
  </template>
@@ -20,28 +20,48 @@ export default {
20
20
  default: true
21
21
  }
22
22
  },
23
- created() {
24
- if (this.autoResize) {
25
- this.resize()
26
- window.addEventListener('resize', this.resize)
23
+ data() {
24
+ return {
25
+ currentColumn: this.column
26
+ }
27
+ },
28
+ watch: {
29
+ column(val) {
30
+ this.currentColumn = val
31
+ },
32
+ autoResize(val) {
33
+ if (val) {
34
+ this.createResize()
35
+ } else {
36
+ this.currentColumn = this.column
37
+ this.destroyResize()
38
+ }
27
39
  }
28
40
  },
41
+ created() {
42
+ if (!this.autoResize) return
43
+ this.createResize()
44
+ },
29
45
  beforeDestroy() {
30
- window.removeEventListener('resize', this.resize)
46
+ this.destroyResize()
31
47
  },
32
48
  methods: {
33
49
  resize() {
34
50
  let wW = document.documentElement.clientWidth
35
51
  if (wW <= 1024) {
36
- this.column = 1
52
+ this.currentColumn = 1
37
53
  } else if (wW > 1024 && wW <= 1600) {
38
- this.column = 2
54
+ this.currentColumn = 2
39
55
  } else if (wW > 1600 && wW <= 1920) {
40
- this.column = 3
56
+ this.currentColumn = 3
41
57
  } else if (wW > 1920) {
42
- this.column = 4
58
+ this.currentColumn = 4
43
59
  }
44
60
  },
61
+ createResize() {
62
+ this.resize()
63
+ window.addEventListener('resize', this.resize)
64
+ },
45
65
  destroyResize() {
46
66
  window.removeEventListener('resize', this.resize)
47
67
  }
@@ -31,10 +31,11 @@
31
31
  | disabled | Boolean | undefined | 是否禁用 |
32
32
  | isClearable | Boolean | false | 是否可清空 |
33
33
  | placeholder | String | '请输入' | 占位提示文本 |
34
- | dNum | Number | undefined | 小数位数,优先级低于format |
35
- | format | String | undefined | 格式化字符串,如'0.00'表示保留2位小数 |
34
+ | dNum | Number | undefined | 小数位数,优先级低于format;超过15位时以字符串保持精度;与format、rangeAuto同时使用时表示最大小数位数 |
35
+ | format | String | undefined | 格式化字符串,如'0.00'表示保留2位小数;与dNum、rangeAuto同时使用时表示最少展示的小数位数 |
36
36
  | suffix | String | '%' | 后缀文本,仅在type='rate'时有效 |
37
37
  | rangeAuto | Boolean | false | 是否自动处理范围值 |
38
+ | isString | Boolean | false | 是否始终以字符串回传 input/change 值;小数位超过15位时自动按 true 处理 |
38
39
 
39
40
  ## 类型说明
40
41
 
@@ -60,6 +61,10 @@
60
61
 
61
62
  ## 事件
62
63
 
64
+ 当 `dNum` 或 `format` 设置为超过 15 位小数时,组件会强制按 `isString=true` 处理,即使未显式配置 `isString`,`input` 和 `change` 也会回传字符串,避免 JavaScript `Number` 丢失精度;失焦时会按最大位数四舍五入并移除尾部 `0`,例如 `0.200000` 会回传 `"0.2"`。设置 `isString` 为 `true` 后,无论小数位数多少,`input` 和 `change` 都回传字符串;清空时回传空字符串。
65
+
66
+ 当 `rangeAuto=true` 且同时配置 `format` 和 `dNum` 时,`format` 表示最少展示的小数位数,`dNum` 表示最多保留的小数位数。例如 `format="0.000000"`、`dNum=20` 时,输入 `0.2` 显示为 `0.200000`,输入 7 至 20 位小数按实际位数显示,超过 20 位时按 20 位四舍五入。
67
+
63
68
  | 事件名 | 说明 | 回调参数 |
64
69
  | ------ | ----------- | ---- |
65
70
  | input | 值改变时触发 | 当前值 |
@@ -109,4 +114,3 @@
109
114
  <!-- 启用步进严格模式 -->
110
115
  <InputNumber v-model="value" type="number" :step="0.5" :stepStrictly="true" />
111
116
  ```
112
-
@@ -33,7 +33,9 @@
33
33
  <script>
34
34
  import emitter from '../../utils/element-ui-emitter'
35
35
  import { $lc } from '../../utils/i18n/index'
36
- import N from '../../utils/numberPor'
36
+ import N, { addDecimal, compareDecimal, decimalToFixed, normalizeDecimal } from '../../utils/numberPor'
37
+
38
+ const MAX_SAFE_DECIMAL_PLACES = 15
37
39
 
38
40
  export default {
39
41
  name: 'InputNumber',
@@ -103,6 +105,10 @@ export default {
103
105
  rangeAuto: {
104
106
  type: Boolean,
105
107
  default: false
108
+ },
109
+ isString: {
110
+ type: Boolean,
111
+ default: false
106
112
  }
107
113
  },
108
114
  data() {
@@ -155,6 +161,20 @@ export default {
155
161
  watch: {
156
162
  value: {
157
163
  handler(val) {
164
+ if (this.isHighPrecision()) {
165
+ if (val === undefined || val === null || val === '') {
166
+ this.valueStr = ''
167
+ } else {
168
+ const normalized = normalizeDecimal(val)
169
+ if (!normalized) {
170
+ this.valueStr = ''
171
+ return
172
+ }
173
+ const formatted = this.normalizeHighPrecisionValue(val)
174
+ this.valueStr = this.type === 'number' ? formatted : N.addThousands(formatted)
175
+ }
176
+ return
177
+ }
158
178
  // number类型不启用千分位格式化,但应用小数位限制
159
179
  if (this.type === 'number') {
160
180
  if (val === undefined || val === null) {
@@ -189,6 +209,45 @@ export default {
189
209
  }
190
210
  },
191
211
  methods: {
212
+ isAdaptivePrecision() {
213
+ return this.rangeAuto && this.format && (this.dNum || this.dNum === 0)
214
+ },
215
+ maxDecimalPlaces() {
216
+ return this.isAdaptivePrecision()
217
+ ? Math.max(this.fNum, this.dNum)
218
+ : this.fNum
219
+ },
220
+ isHighPrecision() {
221
+ return this.maxDecimalPlaces() > MAX_SAFE_DECIMAL_PLACES
222
+ },
223
+ // 高精度小数会强制使用字符串回传,即使 isString 未开启。
224
+ shouldEmitString() {
225
+ return this.isString || this.isHighPrecision()
226
+ },
227
+ normalizeHighPrecisionValue(value) {
228
+ const normalized = normalizeDecimal(value)
229
+ if (!normalized) return ''
230
+
231
+ const decimalPart = normalized.split('.')[1] || ''
232
+ if (this.isAdaptivePrecision()) {
233
+ const originalValue = String(value).replace(/,/g, '').trim()
234
+ const originalFraction = originalValue.match(/\.(\d*)$/)
235
+ const decimalPlaces = originalFraction
236
+ ? originalFraction[1].length
237
+ : decimalPart.length
238
+ if (decimalPlaces < this.fNum) {
239
+ return decimalToFixed(normalized, this.fNum)
240
+ }
241
+ if (decimalPlaces > this.maxDecimalPlaces()) {
242
+ return decimalToFixed(normalized, this.maxDecimalPlaces())
243
+ }
244
+ return decimalToFixed(normalized, decimalPlaces)
245
+ }
246
+ const roundedValue = this.rangeAuto && decimalPart.length >= this.fNum
247
+ ? normalized
248
+ : decimalToFixed(normalized, this.fNum)
249
+ return normalizeDecimal(roundedValue)
250
+ },
192
251
  focusFn() {
193
252
  this.isFocus = true
194
253
  if (!this.disabled && this.valueStr) {
@@ -210,6 +269,24 @@ export default {
210
269
  },
211
270
  stepFn(ev) {
212
271
  if ((ev.code === 'ArrowUp' || ev.code === 'ArrowDown') && !this.disabled) {
272
+ if (this.isHighPrecision()) {
273
+ const currentValue = normalizeDecimal(this.valueStr)
274
+ if (!currentValue) return
275
+
276
+ ev.preventDefault()
277
+ const stepValue = N.toString(this.step)
278
+ let nextValue = addDecimal(currentValue, ev.code === 'ArrowUp' ? stepValue : '-' + stepValue)
279
+ if (compareDecimal(nextValue, this.min) < 0) {
280
+ nextValue = normalizeDecimal(this.min)
281
+ } else if (compareDecimal(nextValue, this.max) > 0) {
282
+ nextValue = normalizeDecimal(this.max)
283
+ }
284
+
285
+ const formatted = this.normalizeHighPrecisionValue(nextValue)
286
+ this.valueStr = this.type === 'number' ? formatted : N.addThousands(formatted)
287
+ return
288
+ }
289
+
213
290
  let val = N(this.valueStr)
214
291
  if (!isNaN(val)) {
215
292
  ev.preventDefault()
@@ -243,14 +320,53 @@ export default {
243
320
  this.changeIng = false
244
321
  })
245
322
 
323
+ if (this.isHighPrecision()) {
324
+ const normalized = normalizeDecimal(valStr)
325
+ if (!normalized) {
326
+ this.valueStr = ''
327
+ const oldValue = this.value
328
+ if (oldValue || oldValue === 0 || oldValue === '0') {
329
+ const emptyValue = this.shouldEmitString() ? '' : undefined
330
+ this.$emit('input', emptyValue)
331
+ this.$emit('change', emptyValue)
332
+ this.dispatch('ElFormItem', 'el.form.change', [emptyValue])
333
+ }
334
+ return
335
+ }
336
+
337
+ let nextValue = this.normalizeHighPrecisionValue(valStr)
338
+
339
+ if (compareDecimal(nextValue, this.min) < 0) {
340
+ nextValue = normalizeDecimal(this.min)
341
+ } else if (compareDecimal(nextValue, this.max) > 0) {
342
+ nextValue = normalizeDecimal(this.max)
343
+ }
344
+
345
+ nextValue = this.normalizeHighPrecisionValue(nextValue)
346
+
347
+ const formatted = this.type === 'number' ? nextValue : N.addThousands(nextValue)
348
+ this.valueStr = formatted
349
+ this.$nextTick(() => {
350
+ const oldValue = normalizeDecimal(this.value)
351
+ const normalizedNextValue = normalizeDecimal(nextValue)
352
+ if (oldValue !== normalizedNextValue) {
353
+ this.$emit('input', nextValue)
354
+ this.$emit('change', nextValue)
355
+ this.dispatch('ElFormItem', 'el.form.change', [nextValue])
356
+ }
357
+ })
358
+ return
359
+ }
360
+
246
361
  let val = N(valStr)
247
362
  if (isNaN(val)) {
248
363
  this.valueStr = ''
249
364
  let oVal = this.value
250
365
  if (oVal || oVal === 0) {
251
- this.$emit('input', undefined)
252
- this.$emit('change', undefined)
253
- this.dispatch('ElFormItem', 'el.form.change', [undefined])
366
+ const emptyValue = this.shouldEmitString() ? '' : undefined
367
+ this.$emit('input', emptyValue)
368
+ this.$emit('change', emptyValue)
369
+ this.dispatch('ElFormItem', 'el.form.change', [emptyValue])
254
370
  }
255
371
  return
256
372
  }
@@ -294,10 +410,12 @@ export default {
294
410
  this.$nextTick(() => {
295
411
  let nVar = Number(nStr)
296
412
  let oVal = this.value
297
- if (oVal !== nVar) {
298
- this.$emit('input', nVar)
299
- this.$emit('change', nVar)
300
- this.dispatch('ElFormItem', 'el.form.change', [nVar])
413
+ const emittedValue = this.shouldEmitString() ? nStr : nVar
414
+ const valueChanged = this.shouldEmitString() ? String(oVal) !== nStr : oVal !== nVar
415
+ if (valueChanged) {
416
+ this.$emit('input', emittedValue)
417
+ this.$emit('change', emittedValue)
418
+ this.dispatch('ElFormItem', 'el.form.change', [emittedValue])
301
419
  }
302
420
  })
303
421
  }
@@ -2,7 +2,7 @@
2
2
  <div>
3
3
  <div v-if="size !== 'mini'" class="flex-box p-t-ss p-b-ss">
4
4
  <statisItem
5
- :type="(label + '合计') | $lc"
5
+ :type="pageTotalLabel"
6
6
  :list="pageList"
7
7
  :show-w="showW"
8
8
  :show-u="showU"
@@ -18,7 +18,7 @@
18
18
  </div>
19
19
  <div v-else class="n20-statis p-t-ss p-b-ss">
20
20
  <span class="m-r">
21
- {{ (label + ':') | $lc }}<span class="n20-primary"> {{ pageSum }}</span> {{ '条' | $lc }}
21
+ {{ label + ':' }}<span class="n20-primary"> {{ pageSum }}</span> {{ '条' | $lc }}
22
22
  </span>
23
23
  <span class="m-r">
24
24
  {{ '全部:' | $lc }}<span class="n20-primary"> {{ totalSun }}</span> {{ '条' | $lc }}
@@ -28,8 +28,10 @@
28
28
  </template>
29
29
 
30
30
  <script>
31
- import { $lc } from '../../utils/i18n/index'
32
31
  import statisItem from './statisItem.vue'
32
+
33
+ import { $lc } from '../../utils/i18n/index'
34
+
33
35
  export default {
34
36
  name: 'Statis',
35
37
  components: {
@@ -70,10 +72,14 @@ export default {
70
72
  },
71
73
  label: {
72
74
  type: String,
73
- default: $lc("已选")
75
+ default: $lc('已选')
74
76
  }
75
77
  },
76
78
  computed: {
79
+ pageTotalLabel() {
80
+ const separator = ['zh', 'ja', 'th'].includes(this._lang || 'zh') ? '' : ' '
81
+ return this.label + separator + $lc('合计')
82
+ },
77
83
  unitAs() {
78
84
  if (!this.showU) {
79
85
  return undefined
@@ -197,6 +197,198 @@ export const subFixed = function (n, decimal = 6) {
197
197
  return XEUtils.toFixed(n, decimal)
198
198
  }
199
199
  }
200
+
201
+ const decimalPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/
202
+
203
+ function padRight(value, length) {
204
+ return value + '0'.repeat(Math.max(0, length - value.length))
205
+ }
206
+
207
+ function padLeft(value, length) {
208
+ return '0'.repeat(Math.max(0, length - value.length)) + value
209
+ }
210
+
211
+ /**
212
+ * 规范化十进制数字字符串,不经过 Number,避免高精度小数丢失精度。
213
+ * @param {number|string} value
214
+ * @returns {string}
215
+ */
216
+ export const normalizeDecimal = function (value) {
217
+ if (value === undefined || value === null || value === '') return ''
218
+
219
+ const stringValue = toString(value).replace(/,/g, '').trim()
220
+ if (!decimalPattern.test(stringValue)) return ''
221
+
222
+ const sign = stringValue.charAt(0) === '-' ? '-' : ''
223
+ const unsignedValue = stringValue.replace(/^[+-]/, '')
224
+ let [integerPart = '0', decimalPart = ''] = unsignedValue.split('.')
225
+ integerPart = integerPart.replace(/^0+(?=\d)/, '') || '0'
226
+ decimalPart = decimalPart.replace(/0+$/, '')
227
+
228
+ return sign + integerPart + (decimalPart ? '.' + decimalPart : '')
229
+ }
230
+
231
+ function compareUnsignedDecimal(a, b) {
232
+ let [aInteger = '0', aDecimal = ''] = a.split('.')
233
+ let [bInteger = '0', bDecimal = ''] = b.split('.')
234
+ aInteger = aInteger.replace(/^0+(?=\d)/, '') || '0'
235
+ bInteger = bInteger.replace(/^0+(?=\d)/, '') || '0'
236
+
237
+ if (aInteger.length !== bInteger.length) {
238
+ return aInteger.length > bInteger.length ? 1 : -1
239
+ }
240
+ if (aInteger !== bInteger) return aInteger > bInteger ? 1 : -1
241
+
242
+ const decimalLength = Math.max(aDecimal.length, bDecimal.length)
243
+ aDecimal = padRight(aDecimal, decimalLength)
244
+ bDecimal = padRight(bDecimal, decimalLength)
245
+ if (aDecimal === bDecimal) return 0
246
+ return aDecimal > bDecimal ? 1 : -1
247
+ }
248
+
249
+ /**
250
+ * 比较两个十进制数字字符串。
251
+ * @param {number|string} left
252
+ * @param {number|string} right
253
+ * @returns {number} -1、0、1
254
+ */
255
+ export const compareDecimal = function (left, right) {
256
+ const a = normalizeDecimal(left)
257
+ const b = normalizeDecimal(right)
258
+ if (!a || !b) return NaN
259
+
260
+ const aNegative = a.charAt(0) === '-'
261
+ const bNegative = b.charAt(0) === '-'
262
+ const aUnsigned = a.replace(/^-/, '')
263
+ const bUnsigned = b.replace(/^-/, '')
264
+
265
+ if (aNegative !== bNegative) return aNegative ? -1 : 1
266
+ const result = compareUnsignedDecimal(aUnsigned, bUnsigned)
267
+ return aNegative ? -result : result
268
+ }
269
+
270
+ function incrementUnsignedDecimal(value) {
271
+ const digits = value.split('')
272
+ for (let i = digits.length - 1; i >= 0; i -= 1) {
273
+ if (digits[i] === '9') {
274
+ digits[i] = '0'
275
+ } else {
276
+ digits[i] = String(Number(digits[i]) + 1)
277
+ return digits.join('')
278
+ }
279
+ }
280
+ return '1' + digits.join('')
281
+ }
282
+
283
+ /**
284
+ * 使用字符串运算按指定小数位四舍五入,不经过 Number。
285
+ * @param {number|string} value
286
+ * @param {number} decimal
287
+ * @returns {string}
288
+ */
289
+ export const decimalToFixed = function (value, decimal = 0) {
290
+ const normalized = normalizeDecimal(value)
291
+ if (!normalized) return ''
292
+
293
+ const precision = Math.max(0, Number(decimal) || 0)
294
+ const negative = normalized.charAt(0) === '-'
295
+ const unsignedValue = normalized.replace(/^-/, '')
296
+ let [integerPart = '0', decimalPart = ''] = unsignedValue.split('.')
297
+ const shouldRound = decimalPart.length > precision && decimalPart.charAt(precision) >= '5'
298
+ decimalPart = padRight(decimalPart.slice(0, precision), precision)
299
+
300
+ if (shouldRound) {
301
+ const incremented = incrementUnsignedDecimal(integerPart + decimalPart)
302
+ integerPart = incremented.slice(0, Math.max(1, incremented.length - precision))
303
+ decimalPart = precision ? padLeft(incremented.slice(-precision), precision) : ''
304
+ }
305
+
306
+ return (negative ? '-' : '') + integerPart + (precision ? '.' + decimalPart : '')
307
+ }
308
+
309
+ function addUnsignedDecimal(left, right) {
310
+ let [leftInteger = '0', leftDecimal = ''] = left.split('.')
311
+ let [rightInteger = '0', rightDecimal = ''] = right.split('.')
312
+ const decimalLength = Math.max(leftDecimal.length, rightDecimal.length)
313
+ leftDecimal = padRight(leftDecimal, decimalLength)
314
+ rightDecimal = padRight(rightDecimal, decimalLength)
315
+
316
+ let carry = 0
317
+ let result = ''
318
+ const leftDigits = (leftInteger + leftDecimal).split('')
319
+ const rightDigits = (rightInteger + rightDecimal).split('')
320
+ const length = Math.max(leftDigits.length, rightDigits.length)
321
+ for (let i = 0; i < length; i += 1) {
322
+ const leftDigit = Number(leftDigits[leftDigits.length - 1 - i] || 0)
323
+ const rightDigit = Number(rightDigits[rightDigits.length - 1 - i] || 0)
324
+ const sum = leftDigit + rightDigit + carry
325
+ result = String(sum % 10) + result
326
+ carry = Math.floor(sum / 10)
327
+ }
328
+ if (carry) result = String(carry) + result
329
+
330
+ const integerLength = result.length - decimalLength
331
+ const integerPart = result.slice(0, Math.max(1, integerLength))
332
+ const decimalPart = decimalLength ? result.slice(-decimalLength) : ''
333
+ return integerPart + (decimalPart ? '.' + decimalPart : '')
334
+ }
335
+
336
+ function subtractUnsignedDecimal(left, right) {
337
+ let [leftInteger = '0', leftDecimal = ''] = left.split('.')
338
+ let [rightInteger = '0', rightDecimal = ''] = right.split('.')
339
+ const decimalLength = Math.max(leftDecimal.length, rightDecimal.length)
340
+ leftDecimal = padRight(leftDecimal, decimalLength)
341
+ rightDecimal = padRight(rightDecimal, decimalLength)
342
+
343
+ let borrow = 0
344
+ let result = ''
345
+ const leftDigits = (leftInteger + leftDecimal).split('')
346
+ const rightDigits = (rightInteger + rightDecimal).split('')
347
+ const length = Math.max(leftDigits.length, rightDigits.length)
348
+ for (let i = 0; i < length; i += 1) {
349
+ let leftDigit = Number(leftDigits[leftDigits.length - 1 - i] || 0) - borrow
350
+ const rightDigit = Number(rightDigits[rightDigits.length - 1 - i] || 0)
351
+ if (leftDigit < rightDigit) {
352
+ leftDigit += 10
353
+ borrow = 1
354
+ } else {
355
+ borrow = 0
356
+ }
357
+ result = String(leftDigit - rightDigit) + result
358
+ }
359
+
360
+ const integerLength = result.length - decimalLength
361
+ const integerPart = result.slice(0, Math.max(1, integerLength)).replace(/^0+(?=\d)/, '') || '0'
362
+ const decimalPart = decimalLength ? result.slice(-decimalLength) : ''
363
+ return integerPart + (decimalPart ? '.' + decimalPart : '')
364
+ }
365
+
366
+ /**
367
+ * 高精度十进制加法,不经过 Number。
368
+ * @param {number|string} left
369
+ * @param {number|string} right
370
+ * @returns {string}
371
+ */
372
+ export const addDecimal = function (left, right) {
373
+ const a = normalizeDecimal(left)
374
+ const b = normalizeDecimal(right)
375
+ if (!a || !b) return ''
376
+
377
+ const aNegative = a.charAt(0) === '-'
378
+ const bNegative = b.charAt(0) === '-'
379
+ const aUnsigned = a.replace(/^-/, '')
380
+ const bUnsigned = b.replace(/^-/, '')
381
+
382
+ if (aNegative === bNegative) {
383
+ const result = addUnsignedDecimal(aUnsigned, bUnsigned)
384
+ return (aNegative ? '-' : '') + normalizeDecimal(result)
385
+ }
386
+
387
+ const comparison = compareUnsignedDecimal(aUnsigned, bUnsigned)
388
+ if (comparison === 0) return '0'
389
+ if (comparison > 0) return (aNegative ? '-' : '') + normalizeDecimal(subtractUnsignedDecimal(aUnsigned, bUnsigned))
390
+ return (bNegative ? '-' : '') + normalizeDecimal(subtractUnsignedDecimal(bUnsigned, aUnsigned))
391
+ }
200
392
  /**
201
393
  * 将数字或字符串格式的数字转换为千位分隔符格式的字符串
202
394
  * @param {number|string} number - 待转换的数字或字符串格式的数字