n20-common-lib 2.22.75 → 2.22.77

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": "2.22.75",
3
+ "version": "2.22.77",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -8,10 +8,10 @@
8
8
  <template>
9
9
  <!-- 金额类型(默认) -->
10
10
  <InputNumber v-model="money" type="money" />
11
-
11
+
12
12
  <!-- 比率类型 -->
13
13
  <InputNumber v-model="rate" type="rate" />
14
-
14
+
15
15
  <!-- 纯数字类型 -->
16
16
  <InputNumber v-model="num" type="number" />
17
17
  </template>
@@ -19,23 +19,24 @@
19
19
 
20
20
  ## 属性说明
21
21
 
22
- | 属性名 | 类型 | 默认值 | 说明 |
23
- | ------------ | ------------- | ----------------- | --------------------------------- |
24
- | value | Number/String | undefined | 绑定值 |
25
- | stringMode | Boolean | false | 高精度模式,非空绑定值和 input/change 事件值使用字符串 |
26
- | type | String | 'money' | 输入框类型,可选值:'money'、'rate'、'number' |
27
- | maxlength | Number | 16 | 最大输入长度 |
28
- | min | Number/String | -9999999999999.99 | 最小值限制;高精度模式可传字符串 |
29
- | max | Number/String | 9999999999999.99 | 最大值限制;高精度模式可传字符串 |
30
- | step | Number/String | 1 | 步进值;高精度模式可传字符串 |
31
- | stepStrictly | Boolean | false | 是否严格按步进值递增/递减 |
32
- | disabled | Boolean | undefined | 是否禁用 |
33
- | isClearable | Boolean | false | 是否可清空 |
34
- | placeholder | String | '请输入' | 占位提示文本 |
35
- | dNum | Number | undefined | 小数位数,优先级低于format |
36
- | format | String | undefined | 格式化字符串,如'0.00'表示保留2位小数 |
37
- | suffix | String | '%' | 后缀文本,仅在type='rate'时有效 |
38
- | rangeAuto | Boolean | false | 是否自动处理范围值 |
22
+ | 属性名 | 类型 | 默认值 | 说明 |
23
+ | ------------ | ------------- | ----------------- | ----------------------------------------------------------------------------------------------------- |
24
+ | value | Number/String | undefined | 绑定值 |
25
+ | stringMode | Boolean | false | 高精度模式,非空绑定值和 input/change 事件值使用字符串 |
26
+ | isString | Boolean | false | stringMode 兼容别名;小数位超过15位时自动按 true 处理 |
27
+ | type | String | 'money' | 输入框类型,可选值:'money'、'rate'、'number' |
28
+ | maxlength | Number | 16 | 最大输入长度 |
29
+ | min | Number/String | -9999999999999.99 | 最小值限制;高精度模式可传字符串 |
30
+ | max | Number/String | 9999999999999.99 | 最大值限制;高精度模式可传字符串 |
31
+ | step | Number/String | 1 | 步进值;高精度模式可传字符串 |
32
+ | stepStrictly | Boolean | false | 是否严格按步进值递增/递减 |
33
+ | disabled | Boolean | undefined | 是否禁用 |
34
+ | isClearable | Boolean | false | 是否可清空 |
35
+ | placeholder | String | '请输入' | 占位提示文本 |
36
+ | dNum | Number | undefined | 小数位数,优先级低于format;有效小数位超过15位时以字符串保持精度;显式字符串模式下与format、rangeAuto同时使用时表示最大小数位数 |
37
+ | format | String | undefined | 格式化字符串,如'0.00'表示保留2位小数;仅在显式字符串模式与dNum、rangeAuto同时使用时表示最少展示的小数位数 |
38
+ | suffix | String | '%' | 后缀文本,仅在type='rate'时有效 |
39
+ | rangeAuto | Boolean | false | 是否自动处理范围值 |
39
40
 
40
41
  ## 类型说明
41
42
 
@@ -61,18 +62,22 @@
61
62
 
62
63
  ## 事件
63
64
 
64
- | 事件名 | 说明 | 回调参数 |
65
- | ------ | ----------- | ---- |
66
- | input | 值改变时触发 | 当前值 |
67
- | change | 值改变且失去焦点时触发 | 当前值 |
68
- | blur | 失去焦点时触发 | - |
69
- | clear | 清空时触发 | - |
65
+ 当有效小数位设置超过 15 位时,组件会自动使用字符串模式,`input` `change` 回传字符串,避免 JavaScript `Number` 丢失精度。设置 `stringMode` 或 `isString` 为 `true` 后,无论小数位数多少,`input` 和 `change` 都回传字符串;清空时保持原有行为,回传 `undefined`。
66
+
67
+ 当显式开启 `stringMode` `isString`,且 `rangeAuto=true`、同时配置 `format` 和更大的 `dNum` 时,`format` 表示最少展示的小数位数,`dNum` 表示最多保留的小数位数。例如 `isString=true`、`format="0.000000"`、`dNum=20` 时,输入 `0.2` 显示为 `0.200000`,输入 7 至 20 位小数按实际位数显示,超过 20 位时按 20 位四舍五入。未显式开启字符串模式时,`format` 继续按历史规则强制模板精度。
68
+
69
+ | 事件名 | 说明 | 回调参数 |
70
+ | ------ | ---------------------- | -------- |
71
+ | input | 值改变时触发 | 当前值 |
72
+ | change | 值改变且失去焦点时触发 | 当前值 |
73
+ | blur | 失去焦点时触发 | - |
74
+ | clear | 清空时触发 | - |
70
75
 
71
76
  ## 方法
72
77
 
73
- | 方法名 | 说明 |
74
- | ----- | -------- |
75
- | focus | 使输入框获得焦点 |
78
+ | 方法名 | 说明 |
79
+ | ------ | ---------------- |
80
+ | focus | 使输入框获得焦点 |
76
81
 
77
82
  ## 使用示例
78
83
 
@@ -113,7 +118,7 @@
113
118
 
114
119
  ### 20 位小数汇率
115
120
 
116
- `stringMode` 默认关闭,原有金额/利率的精度、数值事件保持原样。开启后,输入、回显、步进、范围比较和变更事件使用高精度十进制处理。`input` / `change` 的非空值是字符串,清空为 `undefined`;`blur` / `clear` 事件签名保持原样。
121
+ `stringMode` `isString` 默认关闭,原有金额/利率的精度、数值事件保持原样。任一配置开启,或按原有优先级计算出的有效小数位超过 15 位时,组件会进入高精度字符串模式。输入、回显、步进、范围比较和变更事件使用高精度十进制处理。
117
122
 
118
123
  下面 `cl-*` 是默认注册前缀;业务项目按自己的 `Vue.use(n20, { prefix })` 替换标签。
119
124
 
@@ -141,9 +146,9 @@ export default {
141
146
  </script>
142
147
  ```
143
148
 
144
- - `stringMode` 不改变 `dNum`、`format` 和 `maxlength` 默认值。`format` 仍优先于 `dNum`;有旧 `format` 配置时需同步调整。
149
+ - `stringMode` / `isString` 不改变 `dNum`、`format` 和 `maxlength` 默认值。`format` 仍优先于 `dNum`;有旧 `format` 配置时需同步调整。小数位超过 15 位时即使两个开关都为 `false`,也会自动使用字符串模式。
145
150
  - `maxlength` 统计全部字符,默认 16。20 位小数加上 `0.` 至少需要 22;按整数位、负号和小数点配置总长度。示例的 35 可容纳默认范围的 13 位整数、负号、小数点和 20 位小数。
146
- - 默认按十进制四舍五入到配置位数并补零;`rangeAuto` 开启后至少补到配置位数,并保留更多已有小数位。需要最多 20 位时使用默认 `rangeAuto=false`。
151
+ - 默认按十进制四舍五入到配置位数并移除尾部 `0`;显式字符串模式同时配置 `rangeAuto + format + 更大的 dNum` 时,按“最少 `format`、最多 `dNum`”处理并保留展示所需的尾零。
147
152
  - `min`、`max`、`step` 的字符串形式用于高精度模式;普通模式继续传 Number。`step` 使用正值。`stepStrictly` 的中点方向与原有 `Math.round` 一致。
148
153
  - 可粘贴科学计数法,提交展开为普通十进制字符串。空值、NaN、Infinity、非十进制输入不作为有效数字。
149
154
  - 按上下箭头改变输入框内容,提交时机与旧模式相同,在 change/blur 时同步绑定值。
@@ -36,6 +36,7 @@ import { $lc } from '../../utils/i18n/index'
36
36
  import N from '../../utils/numberPor'
37
37
  import { Decimal, parseDecimal } from '../../utils/decimal'
38
38
 
39
+ const MAX_SAFE_DECIMAL_PLACES = 15
39
40
 
40
41
  export default {
41
42
  name: 'InputNumber',
@@ -50,6 +51,11 @@ export default {
50
51
  type: Boolean,
51
52
  default: false
52
53
  },
54
+ // isString 是 stringMode 的兼容别名;超过安全小数位时会自动生效。
55
+ isString: {
56
+ type: Boolean,
57
+ default: false
58
+ },
53
59
  /**
54
60
  * 输入框类型
55
61
  * - money: 金额类型,默认保留2位小数,启用千分位格式化
@@ -162,7 +168,7 @@ export default {
162
168
  watch: {
163
169
  value: {
164
170
  handler(val) {
165
- if (this.stringMode) {
171
+ if (this.isStringMode()) {
166
172
  this.setStringDisplay(val)
167
173
  return
168
174
  }
@@ -199,21 +205,52 @@ export default {
199
205
  immediate: true
200
206
  },
201
207
  fNum() {
202
- if (this.stringMode) this.setStringDisplay(this.value)
208
+ if (this.isStringMode()) this.setStringDisplay(this.value)
203
209
  }
204
210
  },
205
211
  methods: {
212
+ isAdaptivePrecision() {
213
+ return (this.stringMode || this.isString) &&
214
+ this.rangeAuto &&
215
+ this.format &&
216
+ (this.dNum || this.dNum === 0) &&
217
+ this.dNum > this.fNum
218
+ },
219
+ maxDecimalPlaces() {
220
+ return this.isAdaptivePrecision()
221
+ ? Math.max(this.fNum, this.dNum)
222
+ : this.fNum
223
+ },
224
+ isStringMode() {
225
+ return this.stringMode || this.isString || this.fNum > MAX_SAFE_DECIMAL_PLACES
226
+ },
206
227
  // rangeAuto 允许已有小数位超过配置值,其余情况按配置位数四舍五入。
207
228
  formatStringValue(value, originalValue = value) {
208
229
  const decimal = parseDecimal(value)
209
230
  if (!decimal) return ''
210
231
  // Decimal 会省略尾零,rangeAuto 额外保留普通十进制输入中的原始小数长度。
211
- const fraction = String(originalValue).trim().match(/\.(\d+)$/)
232
+ const fraction = String(originalValue).replace(/,/g, '').trim().match(/\.(\d+)$/)
233
+ const originalPlaces = Math.max(decimal.decimalPlaces(), fraction ? fraction[1].length : 0)
212
234
  const places = this.rangeAuto
213
- ? Math.max(this.fNum, decimal.decimalPlaces(), fraction ? fraction[1].length : 0)
235
+ ? Math.max(
236
+ this.fNum,
237
+ this.isAdaptivePrecision() ? Math.min(originalPlaces, this.maxDecimalPlaces()) : originalPlaces
238
+ )
214
239
  : this.fNum
215
- // 先舍入再输出,让接近零的负数规范化为零,避免显示值与事件值的符号不同。
216
- return decimal.toDecimalPlaces(places).toFixed(places)
240
+ // 先按目标位数四舍五入,避免转成 Number 后丢失精度。
241
+ const fixedValue = decimal.toDecimalPlaces(places).toFixed(places)
242
+ // format + dNum + rangeAuto 场景中,format 是最少位数,dNum 是最多位数。
243
+ // 保留补齐后的尾零,使回显和用户输入的小数位一致。
244
+ if (this.isAdaptivePrecision()) {
245
+ return decimal.toDecimalPlaces(places).isZero()
246
+ ? new Decimal(0).toFixed(places)
247
+ : fixedValue
248
+ }
249
+ // 其他高精度场景保留当前分支的规范化逻辑:移除尾部 0。
250
+ const trimmedValue = fixedValue
251
+ .replace(/(\.\d*?[1-9])0+$/, '$1')
252
+ .replace(/\.0+$/, '')
253
+ return trimmedValue === '-0' ? '0' : trimmedValue
217
254
  },
218
255
  setStringDisplay(value) {
219
256
  const text = this.formatStringValue(value)
@@ -266,7 +303,7 @@ export default {
266
303
  },
267
304
  stepFn(ev) {
268
305
  if ((ev.code === 'ArrowUp' || ev.code === 'ArrowDown') && !this.disabled) {
269
- if (this.stringMode) {
306
+ if (this.isStringMode()) {
270
307
  const value = parseDecimal(this.valueStr)
271
308
  const step = parseDecimal(this.step)
272
309
  if (value && step && step.gt(0)) {
@@ -296,7 +333,7 @@ export default {
296
333
  }
297
334
  },
298
335
  inputFn(valStr) {
299
- if (this.stringMode) {
336
+ if (this.isStringMode()) {
300
337
  // 编辑中的符号和小数点暂时保留,完整值在失焦时统一规范化。
301
338
  if (['', '-', '+', '.', '-.', '+.'].includes(valStr) || parseDecimal(valStr)) {
302
339
  this.preValue = valStr
@@ -318,7 +355,7 @@ export default {
318
355
  this.changeIng = false
319
356
  })
320
357
 
321
- if (this.stringMode) {
358
+ if (this.isStringMode()) {
322
359
  this.changeStringValue(valStr)
323
360
  return
324
361
  }
@@ -1,13 +1,7 @@
1
1
  <template>
2
2
  <div>
3
3
  <div v-if="size !== 'mini'" class="flex-box p-t-ss p-b-ss">
4
- <statisItem
5
- :type="(label + '合计') | $lc"
6
- :list="pageList"
7
- :show-w="showW"
8
- :show-u="showU"
9
- :count-label="countLabel"
10
- />
4
+ <statisItem :type="pageTotalLabel" :list="pageList" :show-w="showW" :show-u="showU" :count-label="countLabel" />
11
5
  <statisItem
12
6
  :type="'全部合计' | $lc"
13
7
  :list="totalList"
@@ -18,7 +12,7 @@
18
12
  </div>
19
13
  <div v-else class="n20-statis p-t-ss p-b-ss">
20
14
  <span class="m-r">
21
- {{ (label + ':') | $lc }}<span class="n20-primary"> {{ pageSum }}</span> {{ '条' | $lc }}
15
+ {{ label + ':' }}<span class="n20-primary"> {{ pageSum }}</span> {{ '条' | $lc }}
22
16
  </span>
23
17
  <span class="m-r">
24
18
  {{ '全部:' | $lc }}<span class="n20-primary"> {{ totalSun }}</span> {{ '条' | $lc }}
@@ -28,9 +22,10 @@
28
22
  </template>
29
23
 
30
24
  <script>
31
- import { $lc } from '../../utils/i18n/index'
32
25
  import statisItem from './statisItem.vue'
33
26
 
27
+ import { $lc } from '../../utils/i18n/index'
28
+
34
29
  export default {
35
30
  name: 'Statis',
36
31
  components: {
@@ -71,15 +66,19 @@ export default {
71
66
  },
72
67
  label: {
73
68
  type: String,
74
- default: $lc("已选")
69
+ default: $lc('已选')
75
70
  }
76
71
  },
77
72
  computed: {
73
+ pageTotalLabel() {
74
+ const separator = ['zh', 'ja', 'th'].includes(this._lang || 'zh') ? '' : ' '
75
+ return this.label + separator + $lc('合计')
76
+ },
78
77
  unitAs() {
79
78
  if (!this.showU) {
80
79
  return undefined
81
80
  } else if (this.showU === true) {
82
- return $lc("")
81
+ return $lc('')
83
82
  } else {
84
83
  return this.showU
85
84
  }
@@ -87,29 +86,29 @@ export default {
87
86
  pageList() {
88
87
  if (this.pageAmount !== undefined) {
89
88
  return [
90
- { label: $lc("笔数"), value: this.pageSum },
89
+ { label: $lc('笔数'), value: this.pageSum },
91
90
  {
92
- label: $lc("金额"),
91
+ label: $lc('金额'),
93
92
  value: this.pageAmount,
94
93
  unit: this.unitAs
95
94
  }
96
95
  ]
97
96
  } else {
98
- return [{ label: $lc("笔数"), value: this.pageSum }]
97
+ return [{ label: $lc('笔数'), value: this.pageSum }]
99
98
  }
100
99
  },
101
100
  totalList() {
102
101
  if (this.totalAmount !== undefined) {
103
102
  return [
104
- { label: $lc("笔数"), value: this.totalSun },
103
+ { label: $lc('笔数'), value: this.totalSun },
105
104
  {
106
- label: $lc("金额"),
105
+ label: $lc('金额'),
107
106
  value: this.totalAmount,
108
107
  unit: this.unitAs
109
108
  }
110
109
  ]
111
110
  } else {
112
- return [{ label: $lc("笔数"), value: this.totalSun }]
111
+ return [{ label: $lc('笔数'), value: this.totalSun }]
113
112
  }
114
113
  }
115
114
  }
@@ -214,6 +214,13 @@ export default {
214
214
  empty,
215
215
  renderer
216
216
  },
217
+ provide() {
218
+ return {
219
+ n20TableProContext: {
220
+ getData: () => this.data || []
221
+ }
222
+ }
223
+ },
217
224
  props: {
218
225
  scrollY: {
219
226
  type: Object,
@@ -1,19 +1,144 @@
1
1
  <template>
2
- <vxe-column :field="$attrs.prop" :title="$attrs.label" v-bind="$attrs">
2
+ <vxe-column :field="$attrs.prop" :title="$attrs.label" v-bind="resolvedColumnAttrs">
3
3
  <OperateBtns slot-scope="{ row, $index }" :btn-list="btnList" :row="row" @command="(c) => $emit(c, row, $index)" />
4
4
  </vxe-column>
5
5
  </template>
6
6
  <script>
7
7
  import OperateBtns from './OperateBtns.vue'
8
8
 
9
+ const DEFAULT_OPERATE_COLUMN_WIDTH = 180
10
+
9
11
  export default {
10
12
  name: 'TableProOperateColumn',
11
13
  components: { OperateBtns },
14
+ inject: {
15
+ n20TableProContext: {
16
+ default: null
17
+ }
18
+ },
12
19
  props: {
13
20
  btnList: {
14
21
  type: Array,
15
22
  default: () => []
16
23
  }
24
+ },
25
+ computed: {
26
+ resolvedColumnAttrs() {
27
+ const attrs = { ...this.$attrs }
28
+ const hasWidth =
29
+ Object.prototype.hasOwnProperty.call(attrs, 'width') &&
30
+ attrs.width !== null &&
31
+ attrs.width !== undefined &&
32
+ attrs.width !== ''
33
+ if (hasWidth) {
34
+ return attrs
35
+ }
36
+
37
+ const minWidthKey = ['minWidth', 'min-width'].find((key) => {
38
+ const value = attrs[key]
39
+ return Object.prototype.hasOwnProperty.call(attrs, key) && value !== null && value !== undefined && value !== ''
40
+ })
41
+ const calculatedWidth = this.calcOperateColumnWidth()
42
+ if (minWidthKey) {
43
+ const minWidth = this.parsePixelWidth(attrs[minWidthKey])
44
+ if (minWidth !== null) {
45
+ attrs[minWidthKey] = Math.max(calculatedWidth, minWidth)
46
+ }
47
+ } else {
48
+ attrs.width = calculatedWidth
49
+ }
50
+ return attrs
51
+ }
52
+ },
53
+ methods: {
54
+ measureOperateText(text) {
55
+ const normalizedText = text === undefined || text === null ? '' : String(text)
56
+ if (!normalizedText) return 0
57
+
58
+ const fallbackWidth = Array.from(normalizedText).reduce(
59
+ (width, char) => width + (/[⺀-鿿]/.test(char) ? 14 : 8),
60
+ 0
61
+ )
62
+ if (typeof document === 'undefined') return fallbackWidth
63
+
64
+ const canvas = document.createElement('canvas')
65
+ const context = canvas.getContext('2d')
66
+ if (!context) return fallbackWidth
67
+
68
+ context.font = '14px PingFang SC, Microsoft YaHei, sans-serif'
69
+ return Math.max(context.measureText(normalizedText).width, fallbackWidth)
70
+ },
71
+ parsePixelWidth(value) {
72
+ if (typeof value === 'number') {
73
+ return Number.isFinite(value) && value >= 0 ? value : null
74
+ }
75
+ if (typeof value !== 'string') return null
76
+
77
+ const normalizedValue = value.trim()
78
+ if (!/^\d+(\.\d+)?(px)?$/.test(normalizedValue)) return null
79
+ const parsedValue = Number.parseFloat(normalizedValue)
80
+ return Number.isFinite(parsedValue) ? parsedValue : null
81
+ },
82
+ calcOperateColumnWidth() {
83
+ const CELL_HORIZONTAL_PADDING = 14
84
+ const BUTTON_GAP = 6
85
+ const CHILDREN_ARROW_WIDTH = 20
86
+ const MORE_BUTTON_WIDTH = 16
87
+ const buttons = (this.btnList || []).filter((button) => button && button.isHas !== false)
88
+ const getButtonWidth = (button) => {
89
+ let width = this.measureOperateText(button.label)
90
+ if (button.children && button.children.length > 0) {
91
+ width += CHILDREN_ARROW_WIDTH
92
+ }
93
+ return width
94
+ }
95
+ const buttonWidths = buttons.map(getButtonWidth)
96
+
97
+ if (buttonWidths.length === 0) {
98
+ return DEFAULT_OPERATE_COLUMN_WIDTH
99
+ }
100
+
101
+ const calcLayoutWidth = (inlineWidths, hasMoreButton = false) => {
102
+ const itemCount = inlineWidths.length + (hasMoreButton ? 1 : 0)
103
+ const contentWidth = inlineWidths.reduce((total, width) => total + width, 0)
104
+ const moreButtonWidth = hasMoreButton ? MORE_BUTTON_WIDTH : 0
105
+ const gapWidth = Math.max(itemCount - 1, 0) * BUTTON_GAP
106
+ return contentWidth + moreButtonWidth + gapWidth + CELL_HORIZONTAL_PADDING
107
+ }
108
+ const rows = this.n20TableProContext?.getData?.() || []
109
+ if (rows.length > 0) {
110
+ const maxRowWidth = rows.reduce((maxWidth, row) => {
111
+ const visibleButtonWidths = buttons
112
+ .filter((button) => this.hasButton(button.isHas, row))
113
+ .map(getButtonWidth)
114
+ const rowWidth = visibleButtonWidths.length > 3
115
+ ? calcLayoutWidth(visibleButtonWidths.slice(0, 2), true)
116
+ : calcLayoutWidth(visibleButtonWidths)
117
+ return Math.max(maxWidth, rowWidth)
118
+ }, 0)
119
+ return Math.max(DEFAULT_OPERATE_COLUMN_WIDTH, Math.ceil(maxRowWidth))
120
+ }
121
+
122
+ const sortedButtonWidths = [...buttonWidths].sort((a, b) => b - a)
123
+ const directButtonsWidth = calcLayoutWidth(sortedButtonWidths.slice(0, 3))
124
+ const collapsedButtonsWidth =
125
+ sortedButtonWidths.length > 3 ? calcLayoutWidth(sortedButtonWidths.slice(0, 2), true) : 0
126
+
127
+ return Math.max(
128
+ DEFAULT_OPERATE_COLUMN_WIDTH,
129
+ Math.ceil(directButtonsWidth),
130
+ Math.ceil(collapsedButtonsWidth)
131
+ )
132
+ },
133
+ hasButton(isHas, row) {
134
+ if (isHas === undefined || isHas === null) return true
135
+ if (typeof isHas === 'boolean') return isHas
136
+ if (typeof isHas === 'function') return isHas(row)
137
+ if (typeof isHas === 'string' || Array.isArray(isHas)) {
138
+ return typeof this.$has === 'function' ? this.$has(isHas) : true
139
+ }
140
+ return false
141
+ }
17
142
  }
18
143
  }
19
144
  </script>