n20-common-lib 3.3.15 → 3.3.17

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.15",
3
+ "version": "3.3.17",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -265,6 +265,7 @@
265
265
  .table-hover-btns {
266
266
  position: absolute;
267
267
  right: 0;
268
+ width: max-content;
268
269
  display: flex;
269
270
  align-items: center;
270
271
  justify-content: flex-end;
@@ -274,6 +275,7 @@
274
275
 
275
276
  .hover-btns-wrapper {
276
277
  display: flex;
278
+ width: max-content;
277
279
  align-items: center;
278
280
  gap: 4px;
279
281
  white-space: nowrap;
@@ -31,8 +31,8 @@
31
31
  | disabled | Boolean | undefined | 是否禁用 |
32
32
  | isClearable | Boolean | false | 是否可清空 |
33
33
  | placeholder | String | '请输入' | 占位提示文本 |
34
- | dNum | Number | undefined | 小数位数,优先级低于format;超过15位时以字符串保持精度 |
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
38
  | isString | Boolean | false | 是否始终以字符串回传 input/change 值;小数位超过15位时自动按 true 处理 |
@@ -63,6 +63,8 @@
63
63
 
64
64
  当 `dNum` 或 `format` 设置为超过 15 位小数时,组件会强制按 `isString=true` 处理,即使未显式配置 `isString`,`input` 和 `change` 也会回传字符串,避免 JavaScript `Number` 丢失精度;失焦时会按最大位数四舍五入并移除尾部 `0`,例如 `0.200000` 会回传 `"0.2"`。设置 `isString` 为 `true` 后,无论小数位数多少,`input` 和 `change` 都回传字符串;清空时回传空字符串。
65
65
 
66
+ 当 `rangeAuto=true` 且同时配置 `format` 和 `dNum` 时,`format` 表示最少展示的小数位数,`dNum` 表示最多保留的小数位数。例如 `format="0.000000"`、`dNum=20` 时,输入 `0.2` 显示为 `0.200000`,输入 7 至 20 位小数按实际位数显示,超过 20 位时按 20 位四舍五入。
67
+
66
68
  | 事件名 | 说明 | 回调参数 |
67
69
  | ------ | ----------- | ---- |
68
70
  | input | 值改变时触发 | 当前值 |
@@ -170,7 +170,7 @@ export default {
170
170
  this.valueStr = ''
171
171
  return
172
172
  }
173
- const formatted = this.normalizeHighPrecisionValue(normalized)
173
+ const formatted = this.normalizeHighPrecisionValue(val)
174
174
  this.valueStr = this.type === 'number' ? formatted : N.addThousands(formatted)
175
175
  }
176
176
  return
@@ -209,8 +209,16 @@ export default {
209
209
  }
210
210
  },
211
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
+ },
212
220
  isHighPrecision() {
213
- return this.fNum > MAX_SAFE_DECIMAL_PLACES
221
+ return this.maxDecimalPlaces() > MAX_SAFE_DECIMAL_PLACES
214
222
  },
215
223
  // 高精度小数会强制使用字符串回传,即使 isString 未开启。
216
224
  shouldEmitString() {
@@ -221,6 +229,20 @@ export default {
221
229
  if (!normalized) return ''
222
230
 
223
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
+ }
224
246
  const roundedValue = this.rangeAuto && decimalPart.length >= this.fNum
225
247
  ? normalized
226
248
  : decimalToFixed(normalized, this.fNum)
@@ -312,7 +334,7 @@ export default {
312
334
  return
313
335
  }
314
336
 
315
- let nextValue = this.normalizeHighPrecisionValue(normalized)
337
+ let nextValue = this.normalizeHighPrecisionValue(valStr)
316
338
 
317
339
  if (compareDecimal(nextValue, this.min) < 0) {
318
340
  nextValue = normalizeDecimal(this.min)
@@ -320,6 +342,8 @@ export default {
320
342
  nextValue = normalizeDecimal(this.max)
321
343
  }
322
344
 
345
+ nextValue = this.normalizeHighPrecisionValue(nextValue)
346
+
323
347
  const formatted = this.type === 'number' ? nextValue : N.addThousands(nextValue)
324
348
  this.valueStr = formatted
325
349
  this.$nextTick(() => {
@@ -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
@@ -166,6 +166,13 @@ export default {
166
166
  empty,
167
167
  renderer
168
168
  },
169
+ provide() {
170
+ return {
171
+ n20TableProContext: {
172
+ getData: () => this.data || []
173
+ }
174
+ }
175
+ },
169
176
  props: {
170
177
  scrollY: {
171
178
  type: Object,
@@ -1,19 +1,153 @@
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 ICON_AND_GAP_WIDTH = 23
86
+ const CHILDREN_ARROW_WIDTH = 20
87
+ const ICON_ONLY_BUTTON_WIDTH = 28
88
+ const MORE_BUTTON_WIDTH = 29
89
+ const buttons = (this.btnList || []).filter((button) => button && button.isHas !== false)
90
+ const getButtonWidth = (button) => {
91
+ if (button.iconOnly && button.icon) {
92
+ return ICON_ONLY_BUTTON_WIDTH
93
+ }
94
+
95
+ let width = this.measureOperateText(button.label)
96
+ if (button.icon) {
97
+ width += ICON_AND_GAP_WIDTH
98
+ }
99
+ if (button.children && button.children.length > 0) {
100
+ width += CHILDREN_ARROW_WIDTH
101
+ }
102
+ return width
103
+ }
104
+ const buttonWidths = buttons.map(getButtonWidth)
105
+
106
+ if (buttonWidths.length === 0) {
107
+ return DEFAULT_OPERATE_COLUMN_WIDTH
108
+ }
109
+
110
+ const calcLayoutWidth = (inlineWidths, hasMoreButton = false) => {
111
+ const itemCount = inlineWidths.length + (hasMoreButton ? 1 : 0)
112
+ const contentWidth = inlineWidths.reduce((total, width) => total + width, 0)
113
+ const moreButtonWidth = hasMoreButton ? MORE_BUTTON_WIDTH : 0
114
+ const gapWidth = Math.max(itemCount - 1, 0) * BUTTON_GAP
115
+ return contentWidth + moreButtonWidth + gapWidth + CELL_HORIZONTAL_PADDING
116
+ }
117
+ const rows = this.n20TableProContext?.getData?.() || []
118
+ if (rows.length > 0) {
119
+ const maxRowWidth = rows.reduce((maxWidth, row) => {
120
+ const visibleButtonWidths = buttons
121
+ .filter((button) => this.hasButton(button.isHas, row))
122
+ .map(getButtonWidth)
123
+ const rowWidth = visibleButtonWidths.length > 3
124
+ ? calcLayoutWidth(visibleButtonWidths.slice(0, 2), true)
125
+ : calcLayoutWidth(visibleButtonWidths)
126
+ return Math.max(maxWidth, rowWidth)
127
+ }, 0)
128
+ return Math.max(DEFAULT_OPERATE_COLUMN_WIDTH, Math.ceil(maxRowWidth))
129
+ }
130
+
131
+ const sortedButtonWidths = [...buttonWidths].sort((a, b) => b - a)
132
+ const directButtonsWidth = calcLayoutWidth(sortedButtonWidths.slice(0, 3))
133
+ const collapsedButtonsWidth =
134
+ sortedButtonWidths.length > 3 ? calcLayoutWidth(sortedButtonWidths.slice(0, 2), true) : 0
135
+
136
+ return Math.max(
137
+ DEFAULT_OPERATE_COLUMN_WIDTH,
138
+ Math.ceil(directButtonsWidth),
139
+ Math.ceil(collapsedButtonsWidth)
140
+ )
141
+ },
142
+ hasButton(isHas, row) {
143
+ if (isHas === undefined || isHas === null) return true
144
+ if (typeof isHas === 'boolean') return isHas
145
+ if (typeof isHas === 'function') return isHas(row)
146
+ if (typeof isHas === 'string' || Array.isArray(isHas)) {
147
+ return typeof this.$has === 'function' ? this.$has(isHas) : true
148
+ }
149
+ return false
150
+ }
17
151
  }
18
152
  }
19
153
  </script>
@@ -287,7 +287,7 @@
287
287
  v-if="isOperateFixed && btnList && btnList.length > 0"
288
288
  :title="$lc('操作')"
289
289
  field="__operate__"
290
- :width="operateColumnWidth"
290
+ :width="computedOperateColumnWidth"
291
291
  fixed="right"
292
292
  align="center"
293
293
  >
@@ -452,8 +452,10 @@ import empty from '../../Empty'
452
452
  import tableShowColumn from '../../ShowColumn/index.vue'
453
453
  import { saveTransform } from '../../ShowColumn/index.vue'
454
454
  import tableOperate from '../../TableOperate/index.vue'
455
- import OperateBtns from '../../TableProOperateColumn/OperateBtns.vue'
456
455
  import { isTableCellInteractiveTarget } from '../../TablePro/cellClick'
456
+ import OperateBtns from '../../TableProOperateColumn/OperateBtns.vue'
457
+
458
+ const DEFAULT_OPERATE_COLUMN_WIDTH = 180
457
459
 
458
460
  const renderer = {
459
461
  name: 'renderer',
@@ -627,10 +629,16 @@ export default {
627
629
  type: Function,
628
630
  default: null
629
631
  },
632
+ // 固定操作列宽度;不传时按各行实际可见按钮的最大宽度自动计算
630
633
  operateColumnWidth: {
631
634
  type: Number,
632
635
  default: 180
633
636
  },
637
+ // 自动计算操作列宽度的下限;显式传入 operateColumnWidth 时不生效
638
+ operateColumnMinWidth: {
639
+ type: [Number, String],
640
+ default: 180
641
+ },
634
642
  // 异步获取操作按钮函数,(row) => Promise<Array<BtnItem>>
635
643
  getOperateBtns: {
636
644
  type: Function,
@@ -722,6 +730,19 @@ export default {
722
730
  const activeCount = [this.showColumn, this.showSetsize, hasExpand].filter(Boolean).length
723
731
  return activeCount === 3 ? 112 : 76
724
732
  },
733
+ // 操作列宽度:显式传入时优先,否则按按钮内容自动计算
734
+ computedOperateColumnWidth() {
735
+ const hasCustomWidth = Object.prototype.hasOwnProperty.call(this.$options.propsData || {}, 'operateColumnWidth')
736
+ const hasCustomWidthValue =
737
+ this.operateColumnWidth !== null && this.operateColumnWidth !== undefined && this.operateColumnWidth !== ''
738
+ const customWidth = hasCustomWidthValue ? Number(this.operateColumnWidth) : NaN
739
+ if (hasCustomWidth && Number.isFinite(customWidth) && customWidth >= 0) {
740
+ return customWidth
741
+ }
742
+ const calculatedWidth = this.calcOperateColumnWidth()
743
+ const minWidth = this.parseOperateColumnPixelWidth(this.operateColumnMinWidth)
744
+ return minWidth === null ? calculatedWidth : Math.max(calculatedWidth, minWidth)
745
+ },
725
746
  // 骨架屏定位样式,根据表头实际高度动态计算
726
747
  skeletonStyle() {
727
748
  return {
@@ -1385,6 +1406,121 @@ export default {
1385
1406
  return checkedSet.has(args.row) ? 'v3-n20-table-pro__row-checked' : ''
1386
1407
  },
1387
1408
 
1409
+ // 测量操作按钮文案宽度,优先使用当前页面字体,服务端或 canvas 不可用时使用保守估算
1410
+ measureOperateText(text) {
1411
+ const normalizedText = text === undefined || text === null ? '' : String(text)
1412
+ if (!normalizedText) return 0
1413
+
1414
+ const fallbackWidth = Array.from(normalizedText).reduce(
1415
+ (width, char) => width + (/[⺀-鿿]/.test(char) ? 14 : 8),
1416
+ 0
1417
+ )
1418
+ if (typeof document === 'undefined') return fallbackWidth
1419
+
1420
+ const canvas = document.createElement('canvas')
1421
+ const context = canvas.getContext('2d')
1422
+ if (!context) return fallbackWidth
1423
+
1424
+ const tableEl = this.$refs.vxeTable?.$el
1425
+ const textEl = tableEl?.querySelector('.table-operate-btns__wrapper .el-link')
1426
+ if (textEl && typeof window !== 'undefined' && window.getComputedStyle) {
1427
+ const style = window.getComputedStyle(textEl)
1428
+ context.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`
1429
+ } else {
1430
+ context.font = '14px PingFang SC, Microsoft YaHei, sans-serif'
1431
+ }
1432
+
1433
+ return Math.max(context.measureText(normalizedText).width, fallbackWidth)
1434
+ },
1435
+
1436
+ parseOperateColumnPixelWidth(value) {
1437
+ if (typeof value === 'number') {
1438
+ return Number.isFinite(value) && value >= 0 ? value : null
1439
+ }
1440
+ if (typeof value !== 'string') return null
1441
+
1442
+ const normalizedValue = value.trim()
1443
+ if (!/^\d+(\.\d+)?(px)?$/.test(normalizedValue)) return null
1444
+ const parsedValue = Number.parseFloat(normalizedValue)
1445
+ return Number.isFinite(parsedValue) ? parsedValue : null
1446
+ },
1447
+
1448
+ // 固定操作列按当前数据中每行实际可见的按钮布局取最大宽度
1449
+ calcOperateColumnWidth() {
1450
+ const CELL_HORIZONTAL_PADDING = 24
1451
+ const BUTTON_GAP = 12
1452
+ const ICON_AND_GAP_WIDTH = 20
1453
+ const CHILDREN_ARROW_WIDTH = 14
1454
+ const ICON_ONLY_BUTTON_WIDTH = 28
1455
+ const MORE_BUTTON_WIDTH = 40
1456
+ const buttons = (this.btnList || []).filter(Boolean)
1457
+ const buttonWidthMap = new Map()
1458
+ buttons.forEach((button) => {
1459
+ if (button.iconOnly && button.icon) {
1460
+ buttonWidthMap.set(button, ICON_ONLY_BUTTON_WIDTH)
1461
+ return
1462
+ }
1463
+
1464
+ let width = this.measureOperateText(button.label)
1465
+ if (button.icon) {
1466
+ width += ICON_AND_GAP_WIDTH
1467
+ }
1468
+ if (button.children && button.children.length > 0) {
1469
+ width += CHILDREN_ARROW_WIDTH
1470
+ }
1471
+ buttonWidthMap.set(button, width)
1472
+ })
1473
+
1474
+ if (buttons.length === 0) {
1475
+ return DEFAULT_OPERATE_COLUMN_WIDTH
1476
+ }
1477
+
1478
+ const calcLayoutWidth = (inlineWidths, hasMoreButton = false) => {
1479
+ const itemCount = inlineWidths.length + (hasMoreButton ? 1 : 0)
1480
+ const buttonWidth = inlineWidths.reduce((total, width) => total + width, 0)
1481
+ const moreButtonWidth = hasMoreButton ? MORE_BUTTON_WIDTH : 0
1482
+ const gapWidth = Math.max(itemCount - 1, 0) * BUTTON_GAP
1483
+ return buttonWidth + moreButtonWidth + gapWidth + CELL_HORIZONTAL_PADDING
1484
+ }
1485
+
1486
+ const calcButtonsWidth = (visibleButtons) => {
1487
+ const hasMoreButton = visibleButtons.length > 3 || !!this.getOperateBtns
1488
+ const inlineButtons = visibleButtons.length > 3 ? visibleButtons.slice(0, 2) : visibleButtons
1489
+ return calcLayoutWidth(
1490
+ inlineButtons.map((button) => buttonWidthMap.get(button) || 0),
1491
+ hasMoreButton
1492
+ )
1493
+ }
1494
+
1495
+ // 直接根据 data 计算所有行,可覆盖 isHas(row) 导致的 1/2/3/更多按钮差异,且不受虚拟滚动影响。
1496
+ const rows = Array.isArray(this.data) ? this.data : []
1497
+ const maxRowWidth = rows.reduce((maxWidth, row) => {
1498
+ const visibleButtons = buttons.filter((button) => this.hasBtn(button.isHas, row))
1499
+ return Math.max(maxWidth, calcButtonsWidth(visibleButtons))
1500
+ }, 0)
1501
+
1502
+ if (maxRowWidth > 0) {
1503
+ return Math.max(DEFAULT_OPERATE_COLUMN_WIDTH, Math.ceil(maxRowWidth))
1504
+ }
1505
+
1506
+ // 数据尚未加载时按所有可能布局保守估算,避免首次渲染回落到过窄列宽。
1507
+ const sortedButtonWidths = buttons
1508
+ .filter((button) => button.isHas !== false)
1509
+ .map((button) => buttonWidthMap.get(button) || 0)
1510
+ .sort((a, b) => b - a)
1511
+ const directButtonsWidth = calcLayoutWidth(sortedButtonWidths.slice(0, 3))
1512
+ const collapsedButtonsWidth =
1513
+ sortedButtonWidths.length > 3 ? calcLayoutWidth(sortedButtonWidths.slice(0, 2), true) : 0
1514
+ const dynamicButtonsWidth = this.getOperateBtns ? calcLayoutWidth(sortedButtonWidths.slice(0, 3), true) : 0
1515
+
1516
+ return Math.max(
1517
+ DEFAULT_OPERATE_COLUMN_WIDTH,
1518
+ Math.ceil(directButtonsWidth),
1519
+ Math.ceil(collapsedButtonsWidth),
1520
+ Math.ceil(dynamicButtonsWidth)
1521
+ )
1522
+ },
1523
+
1388
1524
  calcColumnWidth(columns) {
1389
1525
  // 常量配置
1390
1526
  const CHAR_WIDTH = 20 // 每个字符的平均宽度(像素)