n20-common-lib 3.3.13 → 3.3.15

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.13",
3
+ "version": "3.3.15",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -57,13 +57,17 @@ export default {
57
57
  }
58
58
  },
59
59
  computed: {
60
+ isChinaUrban() {
61
+ return this.isUrban && this.countryId === 'CHN'
62
+ },
60
63
  propsAs() {
61
64
  let _prop = {
62
65
  value: 'regionNo',
63
66
  label: 'regionName',
64
67
  children: 'children',
65
68
  expandTrigger: 'hover',
66
- checkStrictly: !this.isUrban
69
+ // 保留二级城市的子节点后,允许独立选择二级城市和三级地区。
70
+ checkStrictly: this.isChinaUrban || !this.isUrban
67
71
  }
68
72
  return Object.assign(_prop, this.props)
69
73
  },
@@ -75,17 +79,17 @@ export default {
75
79
  },
76
80
  streetKey() {
77
81
  return this.countryId === 'CHN' ? '_china_street_tree_' : `_${this.countryId}_street_tree_`
82
+ },
83
+ cacheKey() {
84
+ // 按当前模式检查缓存,避免普通地区缓存存在时误用尚未加载的城市缓存。
85
+ return this.isUrban ? this.urbanKey : this.isStreet ? this.streetKey : this.areaKey
78
86
  }
79
87
  },
80
88
  mounted() {
81
- if (!window[this.areaKey]) {
89
+ if (!window[this.cacheKey]) {
82
90
  this.getAreaTree()
83
91
  } else {
84
- this.areaTree = this.isUrban
85
- ? window[this.urbanKey]
86
- : this.isStreet
87
- ? window[this.streetKey]
88
- : window[this.areaKey]
92
+ this.areaTree = window[this.cacheKey]
89
93
  }
90
94
  },
91
95
  methods: {
@@ -100,7 +104,7 @@ export default {
100
104
  if (data) {
101
105
  let result = JSON.parse(JSON.stringify(data))
102
106
  if (this.isUrban) {
103
- window[this.urbanKey] = isUrbanFn(result)
107
+ window[this.urbanKey] = isUrbanFn(result, this.isChinaUrban)
104
108
  this.areaTree = window[this.urbanKey]
105
109
  } else if (this.isStreet) {
106
110
  window[this.streetKey] = treeFn(result)
@@ -133,12 +137,21 @@ const treeFn = (data) => {
133
137
  })
134
138
  }
135
139
 
136
- const isUrbanFn = (data) => {
140
+ const isUrbanFn = (data, isChinaUrban = false) => {
137
141
  return data?.map((res) => {
138
- if (res.regionLevel === '2' || !res.children?.length) {
142
+ const regionLevel = String(res.regionLevel)
143
+ // 直辖市只保留一级节点,直接返回接口原有的两位 regionNo,不伪造下级编码。
144
+ const isMunicipality =
145
+ isChinaUrban && regionLevel === '1' && ['11', '12', '31', '50'].includes(String(res.regionNo))
146
+ if (isChinaUrban && regionLevel === '1' && !isMunicipality) {
147
+ // 普通省份仅用于展开;checkStrictly 模式下禁用父节点不影响子节点选择。
148
+ res.disabled = true
149
+ }
150
+ if (isMunicipality || (!isChinaUrban && regionLevel === '2') || !res.children?.length) {
139
151
  res.children = null
140
152
  } else {
141
- res.children = isUrbanFn(res.children)
153
+ // 保留省代管县级市等三级节点,解除原先在二级统一截断的限制。
154
+ res.children = isUrbanFn(res.children, isChinaUrban)
142
155
  }
143
156
  return res
144
157
  })
@@ -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 |
34
+ | dNum | Number | undefined | 小数位数,优先级低于format;超过15位时以字符串保持精度 |
35
35
  | format | String | undefined | 格式化字符串,如'0.00'表示保留2位小数 |
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,8 @@
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
+
63
66
  | 事件名 | 说明 | 回调参数 |
64
67
  | ------ | ----------- | ---- |
65
68
  | input | 值改变时触发 | 当前值 |
@@ -109,4 +112,3 @@
109
112
  <!-- 启用步进严格模式 -->
110
113
  <InputNumber v-model="value" type="number" :step="0.5" :stepStrictly="true" />
111
114
  ```
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(normalized)
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,23 @@ export default {
189
209
  }
190
210
  },
191
211
  methods: {
212
+ isHighPrecision() {
213
+ return this.fNum > MAX_SAFE_DECIMAL_PLACES
214
+ },
215
+ // 高精度小数会强制使用字符串回传,即使 isString 未开启。
216
+ shouldEmitString() {
217
+ return this.isString || this.isHighPrecision()
218
+ },
219
+ normalizeHighPrecisionValue(value) {
220
+ const normalized = normalizeDecimal(value)
221
+ if (!normalized) return ''
222
+
223
+ const decimalPart = normalized.split('.')[1] || ''
224
+ const roundedValue = this.rangeAuto && decimalPart.length >= this.fNum
225
+ ? normalized
226
+ : decimalToFixed(normalized, this.fNum)
227
+ return normalizeDecimal(roundedValue)
228
+ },
192
229
  focusFn() {
193
230
  this.isFocus = true
194
231
  if (!this.disabled && this.valueStr) {
@@ -210,6 +247,24 @@ export default {
210
247
  },
211
248
  stepFn(ev) {
212
249
  if ((ev.code === 'ArrowUp' || ev.code === 'ArrowDown') && !this.disabled) {
250
+ if (this.isHighPrecision()) {
251
+ const currentValue = normalizeDecimal(this.valueStr)
252
+ if (!currentValue) return
253
+
254
+ ev.preventDefault()
255
+ const stepValue = N.toString(this.step)
256
+ let nextValue = addDecimal(currentValue, ev.code === 'ArrowUp' ? stepValue : '-' + stepValue)
257
+ if (compareDecimal(nextValue, this.min) < 0) {
258
+ nextValue = normalizeDecimal(this.min)
259
+ } else if (compareDecimal(nextValue, this.max) > 0) {
260
+ nextValue = normalizeDecimal(this.max)
261
+ }
262
+
263
+ const formatted = this.normalizeHighPrecisionValue(nextValue)
264
+ this.valueStr = this.type === 'number' ? formatted : N.addThousands(formatted)
265
+ return
266
+ }
267
+
213
268
  let val = N(this.valueStr)
214
269
  if (!isNaN(val)) {
215
270
  ev.preventDefault()
@@ -243,14 +298,51 @@ export default {
243
298
  this.changeIng = false
244
299
  })
245
300
 
301
+ if (this.isHighPrecision()) {
302
+ const normalized = normalizeDecimal(valStr)
303
+ if (!normalized) {
304
+ this.valueStr = ''
305
+ const oldValue = this.value
306
+ if (oldValue || oldValue === 0 || oldValue === '0') {
307
+ const emptyValue = this.shouldEmitString() ? '' : undefined
308
+ this.$emit('input', emptyValue)
309
+ this.$emit('change', emptyValue)
310
+ this.dispatch('ElFormItem', 'el.form.change', [emptyValue])
311
+ }
312
+ return
313
+ }
314
+
315
+ let nextValue = this.normalizeHighPrecisionValue(normalized)
316
+
317
+ if (compareDecimal(nextValue, this.min) < 0) {
318
+ nextValue = normalizeDecimal(this.min)
319
+ } else if (compareDecimal(nextValue, this.max) > 0) {
320
+ nextValue = normalizeDecimal(this.max)
321
+ }
322
+
323
+ const formatted = this.type === 'number' ? nextValue : N.addThousands(nextValue)
324
+ this.valueStr = formatted
325
+ this.$nextTick(() => {
326
+ const oldValue = normalizeDecimal(this.value)
327
+ const normalizedNextValue = normalizeDecimal(nextValue)
328
+ if (oldValue !== normalizedNextValue) {
329
+ this.$emit('input', nextValue)
330
+ this.$emit('change', nextValue)
331
+ this.dispatch('ElFormItem', 'el.form.change', [nextValue])
332
+ }
333
+ })
334
+ return
335
+ }
336
+
246
337
  let val = N(valStr)
247
338
  if (isNaN(val)) {
248
339
  this.valueStr = ''
249
340
  let oVal = this.value
250
341
  if (oVal || oVal === 0) {
251
- this.$emit('input', undefined)
252
- this.$emit('change', undefined)
253
- this.dispatch('ElFormItem', 'el.form.change', [undefined])
342
+ const emptyValue = this.shouldEmitString() ? '' : undefined
343
+ this.$emit('input', emptyValue)
344
+ this.$emit('change', emptyValue)
345
+ this.dispatch('ElFormItem', 'el.form.change', [emptyValue])
254
346
  }
255
347
  return
256
348
  }
@@ -294,10 +386,12 @@ export default {
294
386
  this.$nextTick(() => {
295
387
  let nVar = Number(nStr)
296
388
  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])
389
+ const emittedValue = this.shouldEmitString() ? nStr : nVar
390
+ const valueChanged = this.shouldEmitString() ? String(oVal) !== nStr : oVal !== nVar
391
+ if (valueChanged) {
392
+ this.$emit('input', emittedValue)
393
+ this.$emit('change', emittedValue)
394
+ this.dispatch('ElFormItem', 'el.form.change', [emittedValue])
301
395
  }
302
396
  })
303
397
  }
@@ -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 - 待转换的数字或字符串格式的数字