@nstc-business/imes 1.0.36 → 1.0.37

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": "@nstc-business/imes",
3
- "version": "1.0.36",
3
+ "version": "1.0.37",
4
4
  "private": false,
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -141,8 +141,6 @@ export function sumIndicatorScores(nodes) {
141
141
  items.forEach(n => {
142
142
  const val = nodeScoreValue(n)
143
143
  if (val === null) return
144
- const num = Number(val)
145
- if (Number.isNaN(num)) return
146
144
  has = true
147
145
  total += num
148
146
  })
@@ -238,9 +236,7 @@ export function modelResultDigits(modelWrap) {
238
236
  */
239
237
  export function formatModelResultText(val, modelWrap, options = {}) {
240
238
  if (val === '' || val == null) return ''
241
- const num = Number(val)
242
- if (Number.isNaN(num)) return String(val)
243
- let text = formatFixedDecimal(num, modelResultDigits(modelWrap))
239
+ let text = formatFixedDecimal(val, modelResultDigits(modelWrap))
244
240
  if (options.thousands) {
245
241
  text = addThousands(text)
246
242
  }
@@ -254,9 +250,7 @@ export function formatModelResultText(val, modelWrap, options = {}) {
254
250
  */
255
251
  export function formatScoreText(val, options = {}) {
256
252
  if (val === '' || val == null) return ''
257
- const num = Number(val)
258
- if (Number.isNaN(num)) return String(val)
259
- let text = formatFixedDecimal(num, SCORE_DIGITS)
253
+ let text = formatFixedDecimal(val, SCORE_DIGITS)
260
254
  if (options.thousands) {
261
255
  text = addThousands(text)
262
256
  }
@@ -1,24 +1,146 @@
1
1
  <template>
2
- <N20InputNumber
3
- ref="inner"
4
- v-bind="$attrs"
5
- v-on="$listeners"
6
- @copy.native="onCopy"
7
- @cut.native="onCopy"
8
- />
2
+ <div
3
+ class="n20-num-w"
4
+ @mouseenter="isHove = true"
5
+ @mouseleave="isHove = false"
6
+ >
7
+ <el-input
8
+ ref="input"
9
+ v-model="valueStr"
10
+ v-bind="$attrs"
11
+ class="n20-stc"
12
+ :disabled="disabled"
13
+ :placeholder="phd"
14
+ :clearable="isClearable"
15
+ :validate-event="false"
16
+ @focus="focusFn"
17
+ @input="inputFn"
18
+ @blur="blurFn"
19
+ @clear="clearFn"
20
+ @change="commit"
21
+ @keydown.native="stepFn"
22
+ @copy.native="onCopy"
23
+ @cut.native="onCopy"
24
+ >
25
+ <template v-if="type === 'rate'">
26
+ <span v-if="suffixV" slot="suffix" class="el-input__icon">{{
27
+ suffix
28
+ }}</span>
29
+ </template>
30
+ </el-input>
31
+ </div>
9
32
  </template>
10
33
 
11
34
  <script>
12
- import { InputNumber as N20InputNumber } from 'n20-common-lib'
13
- import {
14
- resolveNumberClipboardText,
15
- clearInputMaxlength
16
- } from './numberCopy'
35
+ import { formatFixedDecimal } from './numberFormat'
36
+ import { resolveNumberClipboardText, clearInputMaxlength } from './numberCopy'
37
+ import { addDecimalText } from './numberStep'
38
+ import { addThousands } from '../../common'
39
+
40
+ /**
41
+ * 输入过程允许的中间态(去千分位后校验):空、-、.、1、1.、.5、-1.2 等
42
+ */
43
+ const NUM_RE = /^[+-]?(\d*(\.\d*)?|\.\d*)$/
17
44
 
18
45
  export default {
19
46
  name: 'InputNumber',
20
47
  inheritAttrs: false,
21
- components: { N20InputNumber },
48
+ props: {
49
+ value: {
50
+ type: [Number, String],
51
+ default: undefined
52
+ },
53
+ type: {
54
+ type: String,
55
+ default: 'money',
56
+ validator(v) {
57
+ return ['money', 'rate'].includes(v)
58
+ }
59
+ },
60
+ min: {
61
+ type: Number,
62
+ default: -9999999999999.99
63
+ },
64
+ max: {
65
+ type: Number,
66
+ default: 9999999999999.99
67
+ },
68
+ step: {
69
+ type: Number,
70
+ default: 1
71
+ },
72
+ stepStrictly: {
73
+ type: Boolean,
74
+ default: false
75
+ },
76
+ disabled: {
77
+ type: Boolean,
78
+ default: undefined
79
+ },
80
+ isClearable: {
81
+ type: Boolean,
82
+ default: false
83
+ },
84
+ placeholder: {
85
+ type: String,
86
+ default: undefined
87
+ },
88
+ dNum: {
89
+ type: Number,
90
+ default: undefined
91
+ },
92
+ format: {
93
+ type: String,
94
+ default: undefined
95
+ },
96
+ suffix: {
97
+ type: String,
98
+ default: '%'
99
+ },
100
+ rangeAuto: {
101
+ type: Boolean,
102
+ default: false
103
+ }
104
+ },
105
+ data() {
106
+ return {
107
+ valueStr: '',
108
+ isHove: false,
109
+ isFocus: false,
110
+ /** 输入过程/上次提交保留的精确数字文本(去千分位),回写不经过 Number() */
111
+ preciseText: '',
112
+ preValue: ''
113
+ }
114
+ },
115
+ computed: {
116
+ suffixV() {
117
+ if (!this.isClearable) return true
118
+ if (!this.valueStr) return true
119
+ if (!(this.isHove || this.isFocus)) return true
120
+ return false
121
+ },
122
+ phd() {
123
+ return this.placeholder || '请输入'
124
+ },
125
+ fNum() {
126
+ if (this.format) {
127
+ const i = this.format.indexOf('.')
128
+ return i === -1 ? 0 : this.format.length - i - 1
129
+ }
130
+ if (this.dNum || this.dNum === 0) {
131
+ return this.dNum
132
+ }
133
+ return this.type === 'rate' ? 4 : 2
134
+ }
135
+ },
136
+ watch: {
137
+ value: {
138
+ handler() {
139
+ this.renderFromValue()
140
+ },
141
+ immediate: true
142
+ }
143
+ },
22
144
  mounted() {
23
145
  this.clearMaxlength()
24
146
  },
@@ -57,10 +179,127 @@ export default {
57
179
  )
58
180
  : ''
59
181
  const text = resolveNumberClipboardText(input && input.value, selected)
60
- const clip = (e && e.clipboardData) || (e && e.originalEvent && e.originalEvent.clipboardData)
182
+ const clip =
183
+ (e && e.clipboardData) ||
184
+ (e && e.originalEvent && e.originalEvent.clipboardData)
61
185
  if (!text || text === selected || !clip) return
62
186
  e.preventDefault()
63
187
  clip.setData('text/plain', text)
188
+ },
189
+ /**
190
+ * 内部 input 当前输入(去千分位、去空格)
191
+ */
192
+ rawText() {
193
+ const input = this.$el && this.$el.querySelector('input')
194
+ return input
195
+ ? String(input.value || '')
196
+ .replace(/,/g, '')
197
+ .trim()
198
+ : ''
199
+ },
200
+ /**
201
+ * 外部 value 变化 -> 精确格式化展示
202
+ * 字符串入参直接按十进制处理(formatFixedDecimal 不经过 Number),不丢高精度
203
+ */
204
+ renderFromValue() {
205
+ const val = this.value
206
+ if (val === '' || val == null) {
207
+ this.valueStr = ''
208
+ this.preciseText = ''
209
+ return
210
+ }
211
+ const text = formatFixedDecimal(val, this.fNum)
212
+ this.preciseText = text
213
+ this.valueStr = addThousands(text)
214
+ },
215
+ focusFn() {
216
+ this.isFocus = true
217
+ if (!this.disabled && this.valueStr) {
218
+ this.valueStr = this.valueStr.replace(/,/g, '')
219
+ }
220
+ },
221
+ /**
222
+ * 输入过程只做合法性校验,不格式化(保留完整精度)
223
+ */
224
+ inputFn(valStr) {
225
+ const raw = String(valStr || '')
226
+ .replace(/,/g, '')
227
+ .trim()
228
+ if (raw !== '-' && raw !== '' && !NUM_RE.test(raw)) {
229
+ this.valueStr = this.preValue
230
+ } else {
231
+ this.preValue = valStr
232
+ this.preciseText = raw
233
+ }
234
+ },
235
+ blurFn() {
236
+ this.isFocus = false
237
+ this.commit()
238
+ this.$emit('blur')
239
+ },
240
+ /**
241
+ * 失焦提交:按 fNum 精确四舍五入(字符串路径),回写字符串值,不经过 Number()
242
+ */
243
+ commit() {
244
+ if (this.disabled) return
245
+ const raw = this.preciseText
246
+ if (raw === '' || raw == null) {
247
+ this.valueStr = ''
248
+ const oVal = this.value
249
+ if (oVal || oVal === 0) {
250
+ this.$emit('input', undefined)
251
+ this.$emit('change', undefined)
252
+ }
253
+ return
254
+ }
255
+ let text = formatFixedDecimal(raw, this.fNum)
256
+ // min/max 钳制(值域内 Number 比较安全;钳制值用精确字符串格式化)
257
+ const num = Number(text)
258
+ if (num < this.min) {
259
+ text = formatFixedDecimal(this.min, this.fNum)
260
+ } else if (num > this.max) {
261
+ text = formatFixedDecimal(this.max, this.fNum)
262
+ }
263
+ this.preciseText = text
264
+ this.valueStr = addThousands(text)
265
+ const oVal = this.value
266
+ if (oVal !== text) {
267
+ this.$emit('input', text)
268
+ this.$emit('change', text)
269
+ }
270
+ },
271
+ clearFn() {
272
+ this.$emit('clear')
273
+ },
274
+ /**
275
+ * 上下键步进:基于精确字符串做十进制加减,避免 number 累加丢精度
276
+ */
277
+ stepFn(ev) {
278
+ if (
279
+ (ev.code === 'ArrowUp' || ev.code === 'ArrowDown') &&
280
+ !this.disabled
281
+ ) {
282
+ const raw = this.rawText()
283
+ if (raw === '' || raw === '-' || raw === '.') return
284
+ if (!NUM_RE.test(raw)) return
285
+ ev.preventDefault()
286
+ const delta = ev.code === 'ArrowUp' ? this.step : -this.step
287
+ let text = addDecimalText(raw, delta)
288
+ if (this.stepStrictly) {
289
+ const num = Number(text)
290
+ if (num % this.step !== 0) {
291
+ text = String(Math.round(num / this.step) * this.step)
292
+ }
293
+ }
294
+ const num = Number(text)
295
+ if (num < this.min) {
296
+ text = formatFixedDecimal(this.min, this.fNum)
297
+ } else if (num > this.max) {
298
+ text = formatFixedDecimal(this.max, this.fNum)
299
+ }
300
+ this.preciseText = text
301
+ this.valueStr = addThousands(text)
302
+ }
64
303
  }
65
304
  }
66
305
  }
@@ -13,19 +13,34 @@
13
13
  * 人员:AI && 徐红飞
14
14
  * 入参:val 数字或数字字符串;digits 小数位数
15
15
  * 出参:格式化后的字符串;空值返回 '';非数字字符串原样返回
16
- * 方法内容:先转最短十进制串再按位四舍五入补零,避开原生 toFixed 的精度噪声
16
+ * 方法内容:先转最短十进制串再按位四舍五入补零,避开原生 toFixed 的精度噪声;
17
+ * 字符串输入直接按十进制字符串处理,不经过 Number()(双精度只有约 15~17 位有效数字,
18
+ * 字符串一旦转 number 高精度小数即被舍入,例如 '160835267.0579509644' 会变成
19
+ * 160835267.05795097,精度无法恢复)
17
20
  */
18
21
  export function formatFixedDecimal(val, digits) {
19
22
  if (val === '' || val == null) return ''
20
23
  const raw = typeof val === 'string' ? val.replace(/,/g, '').trim() : val
21
24
  if (raw === '') return ''
22
- if (typeof raw === 'string' && Number.isNaN(Number(raw))) {
23
- return String(val)
25
+ const d = digits == null || digits === '' ? 2 : Number(digits)
26
+ if (!Number.isFinite(d) || d < 0) return String(val)
27
+
28
+ // 字符串数字:直接按十进制字符串四舍五入,不经过 Number()
29
+ if (typeof raw === 'string') {
30
+ if (/[eE]/.test(raw)) {
31
+ // 科学计数法字符串(如 '1.6e8'):转 number 后展开,保持原行为
32
+ const n = Number(raw)
33
+ if (Number.isNaN(n)) return String(val)
34
+ return roundPlainDecimal(numberToPlainString(n), d)
35
+ }
36
+ if (!/^[+-]?(\d+(\.\d*)?|\.\d+)$/.test(raw)) {
37
+ return String(val) // 非数字字符串原样返回
38
+ }
39
+ return roundPlainDecimal(raw, d)
24
40
  }
25
- const n = typeof raw === 'number' ? raw : Number(raw)
41
+
42
+ const n = raw
26
43
  if (Number.isNaN(n)) return String(val)
27
- const d = digits == null || digits === '' ? 2 : Number(digits)
28
- if (!Number.isFinite(d) || d < 0) return String(n)
29
44
  return roundPlainDecimal(numberToPlainString(n), d)
30
45
  }
31
46
 
@@ -43,4 +43,23 @@ if (rounded !== '1.24') {
43
43
  throw new Error('四舍五入 1.239 保留 2 位应为 1.24,实际: ' + rounded)
44
44
  }
45
45
 
46
- console.log('numberFormat.test.js passed')
46
+ // 高精度 number 字面量:双精度只有约 15~17 位有效数字,入参在解析时已舍入为最近可表示值,
47
+ // 格式化结果只能基于该近似值(语言限制,非格式化层可恢复)
48
+ const highPrecNumber = formatFixedDecimal(160835267.0579509644, 10)
49
+ if (highPrecNumber !== '160835267.0579509700') {
50
+ throw new Error('number 入参受双精度限制应为 160835267.0579509700,实际: ' + highPrecNumber)
51
+ }
52
+
53
+ // 高精度数字字符串:不经过 Number(),应完整保留每一位小数
54
+ const highPrecString = formatFixedDecimal('160835267.0579509644', 10)
55
+ if (highPrecString !== '160835267.0579509644') {
56
+ throw new Error('字符串入参应完整保留 160835267.0579509644,实际: ' + highPrecString)
57
+ }
58
+
59
+ // 字符串入参四舍五入进位
60
+ const carry = formatFixedDecimal('9.99999', 4)
61
+ if (carry !== '10.0000') {
62
+ throw new Error('字符串进位 9.99999 保留 4 位应为 10.0000,实际: ' + carry)
63
+ }
64
+
65
+ console.log('numberFormat 用例全部通过')