n20-common-lib 2.22.76 → 2.22.78

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.76",
3
+ "version": "2.22.78",
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,24 +19,24 @@
19
19
 
20
20
  ## 属性说明
21
21
 
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 |
37
- | format | String | undefined | 格式化字符串,如'0.00'表示保留2位小数 |
38
- | suffix | String | '%' | 后缀文本,仅在type='rate'时有效 |
39
- | 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 | 是否自动处理范围值 |
40
40
 
41
41
  ## 类型说明
42
42
 
@@ -62,18 +62,22 @@
62
62
 
63
63
  ## 事件
64
64
 
65
- | 事件名 | 说明 | 回调参数 |
66
- | ------ | ----------- | ---- |
67
- | input | 值改变时触发 | 当前值 |
68
- | change | 值改变且失去焦点时触发 | 当前值 |
69
- | blur | 失去焦点时触发 | - |
70
- | 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 | 清空时触发 | - |
71
75
 
72
76
  ## 方法
73
77
 
74
- | 方法名 | 说明 |
75
- | ----- | -------- |
76
- | focus | 使输入框获得焦点 |
78
+ | 方法名 | 说明 |
79
+ | ------ | ---------------- |
80
+ | focus | 使输入框获得焦点 |
77
81
 
78
82
  ## 使用示例
79
83
 
@@ -114,7 +118,7 @@
114
118
 
115
119
  ### 20 位小数汇率
116
120
 
117
- `stringMode` 和 `isString` 默认关闭,原有金额/利率的精度、数值事件保持原样。任一配置开启,或 `dNum` / `format` 超过 15 位安全小数位时,组件会自动进入高精度字符串模式。输入、回显、步进、范围比较和变更事件使用高精度十进制处理,失焦时按最大位数四舍五入并移除尾部 `0`,例如 `0.200000` 回传 `"0.2"`。
121
+ `stringMode` 和 `isString` 默认关闭,原有金额/利率的精度、数值事件保持原样。任一配置开启,或按原有优先级计算出的有效小数位超过 15 位时,组件会进入高精度字符串模式。输入、回显、步进、范围比较和变更事件使用高精度十进制处理。
118
122
 
119
123
  下面 `cl-*` 是默认注册前缀;业务项目按自己的 `Vue.use(n20, { prefix })` 替换标签。
120
124
 
@@ -144,7 +148,7 @@ export default {
144
148
 
145
149
  - `stringMode` / `isString` 不改变 `dNum`、`format` 和 `maxlength` 默认值。`format` 仍优先于 `dNum`;有旧 `format` 配置时需同步调整。小数位超过 15 位时即使两个开关都为 `false`,也会自动使用字符串模式。
146
150
  - `maxlength` 统计全部字符,默认 16。20 位小数加上 `0.` 至少需要 22;按整数位、负号和小数点配置总长度。示例的 35 可容纳默认范围的 13 位整数、负号、小数点和 20 位小数。
147
- - 默认按十进制四舍五入到配置位数并移除尾部 `0`;`rangeAuto` 开启后至少按配置位数处理,并保留更多已有小数位。
151
+ - 默认按十进制四舍五入到配置位数并移除尾部 `0`;显式字符串模式同时配置 `rangeAuto + format + 更大的 dNum` 时,按“最少 `format`、最多 `dNum`”处理并保留展示所需的尾零。
148
152
  - `min`、`max`、`step` 的字符串形式用于高精度模式;普通模式继续传 Number。`step` 使用正值。`stepStrictly` 的中点方向与原有 `Math.round` 一致。
149
153
  - 可粘贴科学计数法,提交展开为普通十进制字符串。空值、NaN、Infinity、非十进制输入不作为有效数字。
150
154
  - 按上下箭头改变输入框内容,提交时机与旧模式相同,在 change/blur 时同步绑定值。
@@ -209,6 +209,18 @@ export default {
209
209
  }
210
210
  },
211
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
+ },
212
224
  isStringMode() {
213
225
  return this.stringMode || this.isString || this.fNum > MAX_SAFE_DECIMAL_PLACES
214
226
  },
@@ -217,12 +229,24 @@ export default {
217
229
  const decimal = parseDecimal(value)
218
230
  if (!decimal) return ''
219
231
  // Decimal 会省略尾零,rangeAuto 额外保留普通十进制输入中的原始小数长度。
220
- 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)
221
234
  const places = this.rangeAuto
222
- ? 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
+ )
223
239
  : this.fNum
224
- // 先舍入再输出,并移除尾部 0,避免保存值与接口回显值不一致。
240
+ // 先按目标位数四舍五入,避免转成 Number 后丢失精度。
225
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。
226
250
  const trimmedValue = fixedValue
227
251
  .replace(/(\.\d*?[1-9])0+$/, '$1')
228
252
  .replace(/\.0+$/, '')
@@ -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
  }
@@ -174,7 +174,10 @@ export default {
174
174
  provide() {
175
175
  let bigData = this.bigData
176
176
  return {
177
- bigData
177
+ bigData,
178
+ n20TableContext: {
179
+ getData: () => this.data || []
180
+ }
178
181
  }
179
182
  },
180
183
  watch: {
@@ -1,8 +1,8 @@
1
1
  <template>
2
- <el-table-column v-if="!bigData" v-bind="$attrs">
2
+ <el-table-column v-if="!bigData" v-bind="resolvedColumnAttrs">
3
3
  <OperateBtns slot-scope="{ row, $index }" :btn-list="btnList" :row="row" @command="(c) => $emit(c, row, $index)" />
4
4
  </el-table-column>
5
- <ux-table-column v-else :field="$attrs.prop" :title="$attrs.label" v-bind="$attrs">
5
+ <ux-table-column v-else :field="$attrs.prop" :title="$attrs.label" v-bind="resolvedColumnAttrs">
6
6
  <OperateBtns slot-scope="{ row, $index }" :btn-list="btnList" :row="row" @command="(c) => $emit(c, row, $index)" />
7
7
  </ux-table-column>
8
8
  </template>
@@ -10,17 +10,143 @@
10
10
  <script>
11
11
  // import { $lc } from '../../utils/i18n/index'
12
12
  import OperateBtns from './OperateBtns.vue'
13
+
14
+ const DEFAULT_OPERATE_COLUMN_MIN_WIDTH = 80
15
+
13
16
  export default {
14
17
  name: 'TableOperateColumn',
15
18
  components: {
16
19
  OperateBtns
17
20
  },
18
- inject: { bigData: { default: false } },
21
+ inject: {
22
+ bigData: { default: false },
23
+ n20TableContext: { default: null }
24
+ },
19
25
  props: {
20
26
  btnList: {
21
27
  type: Array,
22
28
  default: () => []
23
29
  }
30
+ },
31
+ computed: {
32
+ resolvedColumnAttrs() {
33
+ const attrs = { ...this.$attrs }
34
+ const hasWidth =
35
+ Object.prototype.hasOwnProperty.call(attrs, 'width') &&
36
+ attrs.width !== null &&
37
+ attrs.width !== undefined &&
38
+ attrs.width !== ''
39
+ if (hasWidth) {
40
+ return attrs
41
+ }
42
+
43
+ const minWidthKey = ['minWidth', 'min-width'].find((key) => {
44
+ const value = attrs[key]
45
+ return Object.prototype.hasOwnProperty.call(attrs, key) && value !== null && value !== undefined && value !== ''
46
+ })
47
+ const calculatedWidth = this.calcOperateColumnWidth()
48
+ if (minWidthKey) {
49
+ const minWidth = this.parsePixelWidth(attrs[minWidthKey])
50
+ if (minWidth !== null) {
51
+ attrs[minWidthKey] = Math.max(calculatedWidth, minWidth)
52
+ }
53
+ } else {
54
+ // 避免 Element/UX Table 将大量剩余空间分配给操作列。
55
+ attrs.width = calculatedWidth
56
+ }
57
+ return attrs
58
+ }
59
+ },
60
+ methods: {
61
+ measureOperateText(text) {
62
+ const normalizedText = text === undefined || text === null ? '' : String(text)
63
+ if (!normalizedText) return 0
64
+
65
+ const fallbackWidth = Array.from(normalizedText).reduce(
66
+ (width, char) => width + (/[⺀-鿿]/.test(char) ? 14 : 8),
67
+ 0
68
+ )
69
+ if (typeof document === 'undefined') return fallbackWidth
70
+
71
+ const canvas = document.createElement('canvas')
72
+ const context = canvas.getContext('2d')
73
+ if (!context) return fallbackWidth
74
+
75
+ context.font = '14px PingFang SC, Microsoft YaHei, sans-serif'
76
+ return Math.max(context.measureText(normalizedText).width, fallbackWidth)
77
+ },
78
+ parsePixelWidth(value) {
79
+ if (typeof value === 'number') {
80
+ return Number.isFinite(value) && value >= 0 ? value : null
81
+ }
82
+ if (typeof value !== 'string') return null
83
+
84
+ const normalizedValue = value.trim()
85
+ if (!/^\d+(\.\d+)?(px)?$/.test(normalizedValue)) return null
86
+ const parsedValue = Number.parseFloat(normalizedValue)
87
+ return Number.isFinite(parsedValue) ? parsedValue : null
88
+ },
89
+ calcOperateColumnWidth() {
90
+ const CELL_HORIZONTAL_PADDING = this.bigData ? 20 : 16
91
+ // OperateBtns 使用行内元素,除 6px margin 外还会产生约 4px 的模板空白。
92
+ const BUTTON_GAP = 10
93
+ const CHILDREN_ARROW_WIDTH = 20
94
+ const MORE_BUTTON_WIDTH = 16
95
+ const buttons = (this.btnList || []).filter((button) => button && button.isHas !== false)
96
+ const getButtonWidth = (button) => {
97
+ let width = this.measureOperateText(button.label)
98
+ if (button.children && button.children.length > 0) {
99
+ width += CHILDREN_ARROW_WIDTH
100
+ }
101
+ return width
102
+ }
103
+ const buttonWidths = buttons.map(getButtonWidth)
104
+
105
+ if (buttonWidths.length === 0) {
106
+ return DEFAULT_OPERATE_COLUMN_MIN_WIDTH
107
+ }
108
+
109
+ const calcLayoutWidth = (inlineWidths, hasMoreButton = false) => {
110
+ const itemCount = inlineWidths.length + (hasMoreButton ? 1 : 0)
111
+ const contentWidth = inlineWidths.reduce((total, width) => total + width, 0)
112
+ const moreButtonWidth = hasMoreButton ? MORE_BUTTON_WIDTH : 0
113
+ const gapWidth = Math.max(itemCount - 1, 0) * BUTTON_GAP
114
+ return contentWidth + moreButtonWidth + gapWidth + CELL_HORIZONTAL_PADDING
115
+ }
116
+ const rows = this.n20TableContext?.getData?.() || []
117
+ if (rows.length > 0) {
118
+ const maxRowWidth = rows.reduce((maxWidth, row) => {
119
+ const visibleButtonWidths = buttons
120
+ .filter((button) => this.hasButton(button.isHas, row))
121
+ .map(getButtonWidth)
122
+ const rowWidth = visibleButtonWidths.length > 3
123
+ ? calcLayoutWidth(visibleButtonWidths.slice(0, 2), true)
124
+ : calcLayoutWidth(visibleButtonWidths)
125
+ return Math.max(maxWidth, rowWidth)
126
+ }, 0)
127
+ return Math.max(DEFAULT_OPERATE_COLUMN_MIN_WIDTH, Math.ceil(maxRowWidth))
128
+ }
129
+
130
+ const sortedButtonWidths = [...buttonWidths].sort((a, b) => b - a)
131
+ const directButtonsWidth = calcLayoutWidth(sortedButtonWidths.slice(0, 3))
132
+ const collapsedButtonsWidth =
133
+ sortedButtonWidths.length > 3 ? calcLayoutWidth(sortedButtonWidths.slice(0, 2), true) : 0
134
+
135
+ return Math.max(
136
+ DEFAULT_OPERATE_COLUMN_MIN_WIDTH,
137
+ Math.ceil(directButtonsWidth),
138
+ Math.ceil(collapsedButtonsWidth)
139
+ )
140
+ },
141
+ hasButton(isHas, row) {
142
+ if (isHas === undefined || isHas === null) return true
143
+ if (typeof isHas === 'boolean') return isHas
144
+ if (typeof isHas === 'function') return isHas(row)
145
+ if (typeof isHas === 'string' || Array.isArray(isHas)) {
146
+ return typeof this.$has === 'function' ? this.$has(isHas) : true
147
+ }
148
+ return false
149
+ }
24
150
  }
25
151
  }
26
152
  </script>
@@ -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>