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 +1 -1
- package/src/assets/css/v3/table.scss +2 -0
- package/src/components/InputNumber/README.md +4 -2
- package/src/components/InputNumber/index.vue +27 -3
- package/src/components/Statis/index.vue +10 -4
- package/src/components/TablePro/index.vue +7 -0
- package/src/components/TableProOperateColumn/index.vue +135 -1
- package/src/components/v3/TablePro/index.vue +138 -2
- package/style/index.css +1 -1
- package/theme/blue.css +1 -1
- package/theme/cctcRed.css +1 -1
- package/theme/green.css +1 -1
- package/theme/lightBlue.css +1 -1
- package/theme/orange.css +1 -1
- package/theme/purple.css +1 -1
- package/theme/red.css +1 -1
- package/theme/yellow.css +1 -1
package/package.json
CHANGED
|
@@ -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(
|
|
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.
|
|
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(
|
|
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="
|
|
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
|
-
{{
|
|
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
|
|
@@ -1,19 +1,153 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<vxe-column :field="$attrs.prop" :title="$attrs.label" v-bind="
|
|
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="
|
|
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 // 每个字符的平均宽度(像素)
|