n20-common-lib 3.2.2 → 3.2.4

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.
@@ -1,1435 +1,1443 @@
1
- <template>
2
- <div class="v3-table-pro-wrapper" style="height: 100%; position: relative; overflow: hidden">
3
- <!-- 骨架屏 - 仅覆盖内容区域,不覆盖表头 -->
4
- <div v-if="loading" class="table-pro-skeleton" :style="skeletonStyle">
5
- <div v-for="n in skeletonRows" :key="'skeleton-' + n" class="skeleton-row">
6
- <div class="skeleton-cell skeleton-checkbox"></div>
7
- <div v-for="col in skeletonCols" :key="'col-' + col" class="skeleton-cell"></div>
8
- </div>
9
- </div>
10
- <!-- 表格 -->
11
- <vxe-table
12
- ref="vxeTable"
13
- :key="colsKey"
14
- :align="'center'"
15
- :data="data"
16
- :height="height"
17
- :class="[{ 'cell-default-set--': cellDefault }, 'v3-n20-table-pro']"
18
- :checkbox-config="{
19
- checkField: 'checked',
20
- checkMethod: forbidSelect,
21
- ...$attrs['checkbox-config'],
22
- ...$attrs.checkboxConfig
23
- }"
24
- show-header-overflow
25
- show-overflow
26
- :row-config="{
27
- isHover: true,
28
- useKey: true,
29
- ...$attrs.rowConfig,
30
- ...$attrs['row-config']
31
- }"
32
- :column-config="{
33
- resizable: true,
34
- useKey: true,
35
- ...$attrs.columnConfig,
36
- ...$attrs['column-config']
37
- }"
38
- :virtual-y-config="scrollY"
39
- :sort-config="{
40
- multiple: false,
41
- remote: true,
42
- trigger: 'cell',
43
- ...$attrs.sortConfig,
44
- ...$attrs['sort-config']
45
- }"
46
- :filter-config="{
47
- remote: true,
48
- iconNone: 'n20-icon-xiala-moren',
49
- iconMatch: 'n20-icon-xiala-moren'
50
- }"
51
- :merge-cells="mergeCells"
52
- :row-class-name="getRowClassName"
53
- v-bind="{ size, ...$attrs, ...sizeBind }"
54
- v-on="$listeners"
55
- @sort-change="(val) => customSortMethod(val)"
56
- @filter-change="filterChange"
57
- @checkbox-change="handleSelectionChange"
58
- @cell-click="handleCellClick"
59
- @cell-mouseenter="handleCellMouseEnter"
60
- @cell-mouseleave="handleCellMouseLeave"
61
- @scroll="handleTableScroll"
62
- @toggle-tree-expand="syncExpandState"
63
- @column-resizable-change="resizableChange"
64
- >
65
- <template v-for="(item, i) in _columns">
66
- <slot v-if="item.slotName" :name="item.slotName" :column="item"></slot>
67
- <vxe-column
68
- v-else-if="item.render"
69
- :key="'vxe-table__render_' + i"
70
- v-bind="{ headerAlign: 'center', ...item }"
71
- :formatter="item.formatter ? item.formatter : 'formatName'"
72
- :filters="item.filters"
73
- :filter-render="item.filters && item.filters.length > 0 ? { name: 'FilterInput' } : null"
74
- :title="item.label"
75
- :field="item.prop"
76
- >
77
- <template #header="{ column }">
78
- <div
79
- class="flex-box flex-v flex-c"
80
- @mouseenter="hoverHeaderProp = item.prop"
81
- @mouseleave="hoverHeaderProp = null"
82
- >
83
- <slot name="header" :column="column">{{ column.title }}</slot>
84
- <i
85
- v-if="item.tooltip"
86
- v-title="item.tooltip"
87
- class="n20-icon-xinxitishi vxe-table--column__icon m-l-ss"
88
- ></i>
89
- <!-- 已固定列(fixed='left'):hover 时显示 lock 图标,点击取消固定 -->
90
- <i
91
- v-if="item.fixed === 'left'"
92
- class="v3-icon-lock vxe-table--column__icon m-l-ss pointer color-primary"
93
- @click.stop="unlockColumn(item)"
94
- ></i>
95
- <!-- 未固定列(且非静态列):hover 时显示 unlock 图标,点击设为固定 -->
96
- <i
97
- v-if="!item.fixed && !item.static"
98
- v-show="hoverHeaderProp === item.prop"
99
- class="v3-icon-unlock vxe-table--column__icon m-l-ss pointer"
100
- @click.stop="lockColumn(item)"
101
- ></i>
102
- </div>
103
- </template>
104
- <template slot-scope="scope">
105
- <renderer :render-content="item.render" :scope="scope" />
106
- </template>
107
- </vxe-column>
108
- <vxe-column
109
- v-else-if="item.type === 'radio'"
110
- :key="'vxe-table-radio-' + i"
111
- v-bind="item"
112
- :title="item.label"
113
- :field="item.prop"
114
- type="radio"
115
- :width="item.width || 50"
116
- :fixed="item.fixed"
117
- />
118
- <vxe-column
119
- v-else-if="item.type === 'checkbox'"
120
- :key="'vxe-table-' + i"
121
- v-bind="item"
122
- :title="item.label"
123
- :field="item.prop"
124
- type="checkbox"
125
- width="50"
126
- :fixed="item.fixed || item.fixed === '' ? item.fixed : 'left'"
127
- >
128
- <template #header="{ $table, checked, indeterminate, disabled }">
129
- <span class="custom-checkbox" @click.stop="toggleAll($table, disabled)">
130
- <el-checkbox
131
- v-if="indeterminate"
132
- :key="key + '_header_indeterminate'"
133
- :indeterminate="indeterminate"
134
- :disabled="disabled"
135
- />
136
- <el-checkbox v-else-if="checked" :key="key + '_header_checked'" :value="checked" :disabled="disabled" />
137
- <el-checkbox v-else :key="key + '_header_unchecked'" :value="checked" :disabled="disabled" />
138
- </span>
139
- </template>
140
- <template #checkbox="{ $table, row, checked, indeterminate, disabled }">
141
- <span class="custom-checkbox" @click.stop="toggleChecked($table, row, disabled)">
142
- <el-checkbox
143
- v-if="indeterminate"
144
- :key="key + '-row-indeterminate'"
145
- :indeterminate="indeterminate"
146
- :disabled="disabled"
147
- />
148
- <el-checkbox v-else-if="checked" :key="key + '-row-checked'" :value="checked" :disabled="disabled" />
149
- <el-checkbox v-else :key="key + '-row-unchecked'" :value="checked" :disabled="disabled" />
150
- </span>
151
- </template>
152
- </vxe-column>
153
- <vxe-colgroup
154
- v-else-if="item.children && item.children.length > 0"
155
- :key="'vxe-colgroup-' + i"
156
- :title="item.label"
157
- >
158
- <vxe-column
159
- v-for="(subItem, i) in item.children"
160
- :key="'vxe-table-' + i"
161
- :formatter="subItem.formatter ? subItem.formatter : 'formatName'"
162
- :filters="subItem.filters"
163
- :filter-render="subItem.filterRender"
164
- :title="subItem.label"
165
- :field="subItem.prop"
166
- v-bind="{ headerAlign: 'center', ...subItem }"
167
- :class-name="`${subItem.wrap && `vxe-table-custom-wrap`} ${subItem.bold && `font-w600`}`"
168
- />
169
- </vxe-colgroup>
170
- <vxe-column
171
- v-else-if="item.formatter"
172
- :key="'vxe-table-base__column_formatter' + i"
173
- :class-name="`${item.wrap && `vxe-table-custom-wrap`} ${item.bold && `font-w600`}`"
174
- :formatter="item.formatter ? item.formatter : 'formatName'"
175
- :filters="item.filters"
176
- :filter-render="item.filters && item.filters.length > 0 ? { name: 'FilterInput' } : null"
177
- :title="item.label"
178
- :field="item.prop"
179
- v-bind="{ headerAlign: 'center', ...item }"
180
- >
181
- <template #header="{ column }">
182
- <div
183
- class="flex-box flex-v flex-c"
184
- @mouseenter="hoverHeaderProp = item.prop"
185
- @mouseleave="hoverHeaderProp = null"
186
- >
187
- <span
188
- v-if="item.tooltip"
189
- v-title="typeof item.tooltip === 'function' ? item.tooltip(column) : item.tooltip"
190
- class="pointer"
191
- style="border-bottom: 1px dashed"
192
- >
193
- <slot :name="`header_${column.field}`" :column="column">
194
- {{ column.title }}
195
- </slot>
196
- </span>
197
- <template v-else>
198
- <slot :name="`header_${column.field}`" :column="column">
199
- {{ column.title }}
200
- </slot>
201
- </template>
202
- <!-- 已固定列(fixed='left'):hover 时显示 lock 图标,点击取消固定 -->
203
- <i
204
- v-if="item.fixed === 'left'"
205
- class="v3-icon-lock vxe-table--column__icon m-l-ss pointer color-primary"
206
- @click.stop="unlockColumn(item)"
207
- ></i>
208
- <!-- 未固定列(且非静态列):hover 时显示 unlock 图标,点击设为固定 -->
209
- <i
210
- v-if="!item.fixed && !item.static"
211
- v-show="hoverHeaderProp === item.prop"
212
- class="v3-icon-unlock vxe-table--column__icon m-l-ss pointer"
213
- @click.stop="lockColumn(item)"
214
- ></i>
215
- </div>
216
- </template>
217
- </vxe-column>
218
- <vxe-column
219
- v-else
220
- :key="'vxe-table-base__column' + i"
221
- :class-name="`${item.wrap && `vxe-table-custom-wrap`} ${item.bold && `font-w600`}`"
222
- :filters="item.filters"
223
- :filter-render="item.filters && item.filters.length > 0 ? { name: 'FilterInput' } : null"
224
- :title="item.label"
225
- :field="item.prop"
226
- v-bind="{ headerAlign: 'center', ...item }"
227
- >
228
- <template #header="{ column }">
229
- <div
230
- class="flex-box flex-v flex-c"
231
- @mouseenter="hoverHeaderProp = item.prop"
232
- @mouseleave="hoverHeaderProp = null"
233
- >
234
- <span
235
- v-if="item.tooltip"
236
- v-title="typeof item.tooltip === 'function' ? item.tooltip(column) : item.tooltip"
237
- class="pointer"
238
- style="border-bottom: 1px dashed"
239
- >
240
- <slot :name="`header_${column.field}`" :column="column">
241
- {{ column.title }}
242
- </slot>
243
- </span>
244
- <template v-else>
245
- <slot :name="`header_${column.field}`" :column="column">
246
- {{ column.title }}
247
- </slot>
248
- </template>
249
- <!-- 已固定列(fixed='left'):hover 时显示 lock 图标,点击取消固定 -->
250
- <i
251
- v-if="item.fixed === 'left'"
252
- class="v3-icon-lock vxe-table--column__icon m-l-ss pointer color-primary"
253
- @click.stop="unlockColumn(item)"
254
- ></i>
255
- <!-- 未固定列(且非静态列):hover 时显示 unlock 图标,点击设为固定 -->
256
- <i
257
- v-if="!item.fixed && !item.static"
258
- v-show="hoverHeaderProp === item.prop"
259
- class="v3-icon-unlock vxe-table--column__icon m-l-ss pointer"
260
- @click.stop="lockColumn(item)"
261
- ></i>
262
- </div>
263
- </template>
264
- <!-- 处理旧格式 formatter 和 render -->
265
- <template v-if="item.type !== 'seq'" #default="{ row, column }">
266
- <!-- 纯对象字段 -->
267
- <template
268
- v-if="row[column.property] && typeof row[column.property] === 'object' && row[column.property].name"
269
- >
270
- {{ row[column.property].name }}
271
- </template>
272
- <slot v-else :name="`cell_${item.prop}`" :row="row">{{
273
- row[column.property] || row[column.property] === 0 ? row[column.property] : '--'
274
- }}</slot>
275
- </template>
276
- </vxe-column>
277
- </template>
278
- <!-- 固定操作列 -->
279
- <vxe-column
280
- v-if="isOperateFixed && btnList && btnList.length > 0"
281
- :title="$lc('操作')"
282
- field="__operate__"
283
- :width="operateColumnWidth"
284
- fixed="right"
285
- align="center"
286
- >
287
- <template #header>
288
- <div class="flex-box flex-v flex-c">
289
- {{ $lc('操作') }}
290
- <i class="v3-icon-unlock vxe-table--column__icon m-l-ss pointer" @click.stop="unfixOperateColumn"></i>
291
- </div>
292
- </template>
293
- <template #default="{ row, $rowIndex }">
294
- <OperateBtns
295
- :btn-list="btnList"
296
- :row="row"
297
- :get-operate-btns="getOperateBtns"
298
- @command="(c) => handleOperateCommand(c, row, $rowIndex)"
299
- />
300
- </template>
301
- </vxe-column>
302
- <template #empty>
303
- <slot name="empty">
304
- <empty type="noData" :content="$lc('暂无数据')" :height="100" :width="100" />
305
- </slot>
306
- </template>
307
- </vxe-table>
308
- <!-- 操作列 -->
309
- <tableOperate
310
- :show-column="showColumn"
311
- :show-setsize="showSetsize"
312
- :show-pin-btn="btnList && btnList.length > 0"
313
- :is-operate-fixed="isOperateFixed"
314
- :size="sizeC"
315
- v-bind="$attrs"
316
- @update:size="sizeUp"
317
- @resize="sizeSet"
318
- @toggle-expand="toggleExpand"
319
- @toggle-operate-fixed="handleToggleOperateFixed"
320
- @visible-column="handleVisibleColumn"
321
- />
322
- <!-- 操作列的悬浮按钮组 -->
323
- <transition name="hover-btns-fade">
324
- <div
325
- v-if="showHoverBtns && visibleHoverBtns.length > 0 && !isOperateFixed"
326
- class="table-hover-btns"
327
- :style="hoverBtnsStyle"
328
- @mouseenter="handleBtnGroupEnter"
329
- @mouseleave="handleBtnGroupLeave"
330
- >
331
- <div class="hover-btns-wrapper">
332
- <template v-for="(btn, idx) in displayHoverBtns">
333
- <!-- 纯图标按钮:与固定列的 el-link 图标按钮样式一致 -->
334
- <el-tooltip
335
- v-if="btn.iconOnly && btn.icon"
336
- :key="'display-' + idx"
337
- :content="btn.tip || btn.label"
338
- placement="top"
339
- >
340
- <el-link
341
- :type="btn.type || 'primary'"
342
- :disabled="resolveDisabled(btn)"
343
- :underline="false"
344
- :class="['hover-btn-icon-only', btn.class]"
345
- @click.stop="handleHoverBtnClick(btn, $event)"
346
- >
347
- <i :class="btn.icon" />
348
- </el-link>
349
- </el-tooltip>
350
- <!-- 图标+文字按钮 -->
351
- <el-button
352
- v-else-if="btn.icon"
353
- :key="'display-icon-' + idx"
354
- :type="btn.type || 'primary'"
355
- :size="btn.size || 'mini'"
356
- :disabled="resolveDisabled(btn)"
357
- :class="[btn.class]"
358
- @click.stop="handleHoverBtnClick(btn, $event)"
359
- >
360
- <i :class="btn.icon" class="m-r-ss" />
361
- <el-badge
362
- v-if="resolveBadge(btn)"
363
- :value="resolveBadge(btn)"
364
- :max="btn.badgeMax || 99"
365
- class="hover-btn-badge"
366
- >
367
- {{ btn.label }}
368
- </el-badge>
369
- <template v-else>{{ btn.label }}</template>
370
- </el-button>
371
- <!-- 纯文字按钮 -->
372
- <el-button
373
- v-else
374
- :key="'display-text-' + idx"
375
- :type="btn.type || 'primary'"
376
- :size="btn.size || 'mini'"
377
- :disabled="resolveDisabled(btn)"
378
- :class="[btn.class]"
379
- @click.stop="handleHoverBtnClick(btn, $event)"
380
- >
381
- <el-badge
382
- v-if="resolveBadge(btn)"
383
- :value="resolveBadge(btn)"
384
- :max="btn.badgeMax || 99"
385
- class="hover-btn-badge"
386
- >
387
- {{ btn.label }}
388
- </el-badge>
389
- <template v-else>{{ btn.label }}</template>
390
- </el-button>
391
- </template>
392
- <!-- 更多按钮下拉菜单 -->
393
- <el-dropdown
394
- v-if="moreHoverBtns.length > 0"
395
- class="hover-btns-more"
396
- @command="handleMoreBtnCommand"
397
- @visible-change="handleDropdownVisibleChange"
398
- >
399
- <el-button type="primary" size="mini">
400
- {{ $lc('更多') }}<i class="el-icon-arrow-down el-icon--right"></i>
401
- </el-button>
402
- <el-dropdown-menu slot="dropdown">
403
- <el-dropdown-item
404
- v-for="(btn, idx) in moreHoverBtns"
405
- :key="'more-' + idx"
406
- :command="btn"
407
- :disabled="resolveDisabled(btn)"
408
- >
409
- <i v-if="btn.icon" :class="btn.icon" class="m-r-ss" />
410
- <el-badge v-if="resolveBadge(btn)" :value="resolveBadge(btn)" :max="btn.badgeMax || 99">
411
- <span :class="btn.type === 'danger' ? 'color-danger' : ''">
412
- {{ btn.label }}
413
- </span>
414
- </el-badge>
415
- <span v-else :class="btn.type === 'danger' ? 'color-danger' : ''">
416
- {{ btn.label }}
417
- </span>
418
- </el-dropdown-item>
419
- </el-dropdown-menu>
420
- </el-dropdown>
421
- </div>
422
- </div>
423
- </transition>
424
-
425
- <table-show-column
426
- ref="showColumn"
427
- :dialog-visible.sync="dialogVisible"
428
- :check-columns="checkColumns"
429
- :columns="columns"
430
- :label-key="labelKey"
431
- :auto-save="autoSave"
432
- :page-id="pageId"
433
- :is-export="isExport"
434
- :is-filter="isFilter"
435
- :width="width"
436
- :has-p-x="hasPX"
437
- @setColumns="setColumns"
438
- />
439
- </div>
440
- </template>
441
-
442
- <script>
443
- import axios from '../../../utils/axios'
444
- import empty from '../../Empty'
445
- import tableShowColumn from '../../ShowColumn/index.vue'
446
- import { saveTransform } from '../../ShowColumn/index.vue'
447
- import tableOperate from '../../TableOperate/index.vue'
448
- import OperateBtns from '../../TableProOperateColumn/OperateBtns.vue'
449
-
450
- const renderer = {
451
- name: 'renderer',
452
- props: {
453
- renderContent: {
454
- type: Function
455
- },
456
- scope: {
457
- type: Object
458
- }
459
- },
460
- render(h) {
461
- return this.renderContent(h, this.scope.row, this.scope)
462
- }
463
- }
464
- export default {
465
- name: 'TableProV3',
466
- components: {
467
- tableOperate,
468
- tableShowColumn,
469
- empty,
470
- renderer,
471
- OperateBtns
472
- },
473
- props: {
474
- scrollY: {
475
- type: Object,
476
- default: () => {
477
- return { enabled: true, gt: 0, model: 'wheel' }
478
- }
479
- },
480
- data: {
481
- type: Array,
482
- default: undefined
483
- },
484
- columns: {
485
- type: Array,
486
- require: true,
487
- default: () => []
488
- },
489
- height: {
490
- type: [String, Number],
491
- default: undefined
492
- },
493
- size: {
494
- type: String,
495
- default: 'small'
496
- },
497
- showSetsize: {
498
- type: Boolean,
499
- default: true
500
- },
501
- cellDefault: {
502
- type: Boolean,
503
- default: true
504
- },
505
- // 禁止选择
506
- forbidSelect: {
507
- type: Function,
508
- default: () => {
509
- const forbidSelect = function ({ row }) {
510
- return row
511
- }
512
- return forbidSelect
513
- }
514
- },
515
- // 是否显示显示列
516
- showColumn: {
517
- type: Boolean,
518
- default: true
519
- },
520
- // 数据更新时是否自动清空已选
521
- clearSelect: {
522
- type: Boolean,
523
- default: true
524
- },
525
- // 筛选条件只输出当前筛选的单个条件,默认为全部条件
526
- isFiliterSingle: {
527
- type: Boolean,
528
- default: false
529
- },
530
- isAutoWidth: {
531
- type: Boolean,
532
- default: true
533
- },
534
- // 合并单元格配置,格式: [{ row: 0, col: 0, rowspan: 2, colspan: 0 }]
535
- mergeCells: {
536
- type: Array,
537
- default: () => []
538
- },
539
- // 全选时是否排除被合并的行(从属行)
540
- excludeMergedRows: {
541
- type: Boolean,
542
- default: false
543
- },
544
- // 是否显示骨架屏加载状态
545
- loading: {
546
- type: Boolean,
547
- default: false
548
- },
549
- // 骨架屏行数
550
- skeletonRows: {
551
- type: Number,
552
- default: 10
553
- },
554
- // 骨架屏列数
555
- skeletonCols: {
556
- type: Number,
557
- default: 6
558
- },
559
- // 悬浮按钮列表配置
560
- // 格式: [{ command: 'edit', label: '编辑', type: 'text', isHas: 'edit', click: (row) => {} }]
561
- btnList: {
562
- type: Array,
563
- default: () => []
564
- },
565
- // 权限检查函数,返回当前行允许显示的按钮 key 列表
566
- // 格式: (row, btns) => ['edit', 'delete'] 或 true(显示所有)或 false(不显示任何)
567
- hoverBtnsPermission: {
568
- type: Function,
569
- default: null
570
- },
571
- // 悬浮按钮组是否显示分隔符
572
- hoverBtnsDivider: {
573
- type: Boolean,
574
- default: true
575
- },
576
- // 悬浮按钮组显示延迟(毫秒)
577
- hoverBtnsDelay: {
578
- type: Number,
579
- default: 50
580
- },
581
- // 悬浮按钮组隐藏延迟(毫秒)
582
- hoverBtnsHideDelay: {
583
- type: Number,
584
- default: 100
585
- },
586
- // 悬浮按钮组最大显示数量,超过则收起到"更多"下拉菜单
587
- hoverBtnsMaxShow: {
588
- type: Number,
589
- default: 4
590
- },
591
- // 列头标签键
592
- labelKey: {
593
- type: String,
594
- default: 'label'
595
- },
596
- // 是否自动保存列设置
597
- autoSave: {
598
- type: Boolean,
599
- default: true
600
- },
601
- pageId: {
602
- type: String,
603
- default: undefined
604
- },
605
- isFilter: {
606
- type: Boolean,
607
- default: false
608
- },
609
- width: {
610
- type: String,
611
- default: '1000px'
612
- },
613
- hasPX: {
614
- type: Boolean,
615
- default: false
616
- },
617
- // 导出函数
618
- exportFn: {
619
- type: Function,
620
- default: null
621
- },
622
- operateColumnWidth: {
623
- type: Number,
624
- default: 180
625
- },
626
- // 异步获取操作按钮函数,(row) => Promise<Array<BtnItem>>
627
- getOperateBtns: {
628
- type: Function,
629
- default: null
630
- }
631
- },
632
- data() {
633
- let _this = this
634
- return {
635
- colsKey: 0,
636
- sizeC: localStorage.getItem('table-size') || _this.size,
637
- sizeBind: undefined,
638
- setTableSize: false,
639
- key: 0,
640
- // 悬浮按钮组相关状态
641
- hoverRowData: null, // 当前悬停行数据
642
- hoverRowIndex: -1, // 当前悬停行索引
643
- showHoverBtns: false, // 是否显示悬浮按钮组
644
- hoverBtnsPosition: { top: 0, left: 0, width: 0 }, // 按钮组位置
645
- hoverShowTimer: null, // 显示延迟定时器
646
- hoverHideTimer: null, // 隐藏延迟定时器
647
- isHoverOnBtnGroup: false, // 鼠标是否在按钮组上
648
- isDropdownVisible: false, // "更多"下拉菜单是否展开
649
- isOperateFixed: true, // 操作按钮是否固定为列模式
650
- hoverHeaderProp: null, // 当前悬停表头列的 prop
651
- hoverIconKey: null, // 当前悬停的静态列头图标标识
652
- isExpand: false,
653
- isExport: false, // 是否导出
654
- dialogVisible: false, // 显示列设置对话框
655
- checkColumns: this.filterDefaultHidden(this.columns), // 显示列设置对话框中已选择的列(默认过滤 defaultHidden 列)
656
- headerHeight: 40 // 表头实际高度,骨架屏定位用
657
- }
658
- },
659
- computed: {
660
- _columns: {
661
- get() {
662
- // 当操作列已通过模板固定渲染时,过滤掉 checkColumns 中的操作列,避免重复
663
- const cols =
664
- this.isOperateFixed && this.btnList && this.btnList.length > 0
665
- ? this.checkColumns.filter((col) => !(col.static && col.label === this.$lc('操作')))
666
- : this.checkColumns
667
- if (this.isAutoWidth) {
668
- return this.calcColumnWidth(cols)
669
- } else {
670
- return cols
671
- }
672
- },
673
- set(val) {
674
- return val
675
- }
676
- },
677
- // 计算当前行可见的悬浮按钮
678
- visibleHoverBtns() {
679
- if (!this.hoverRowData || !this.btnList || this.btnList.length === 0) {
680
- return []
681
- }
682
- return this.btnList.filter((btn) => this.hasBtn(btn.isHas, this.hoverRowData))
683
- },
684
- // 直接显示的按钮(不超过阈值)
685
- displayHoverBtns() {
686
- if (this.visibleHoverBtns.length > this.hoverBtnsMaxShow) {
687
- return this.visibleHoverBtns.slice(0, this.hoverBtnsMaxShow - 1)
688
- }
689
- return this.visibleHoverBtns
690
- },
691
- // 收起到"更多"下拉菜单的按钮
692
- moreHoverBtns() {
693
- if (this.visibleHoverBtns.length > this.hoverBtnsMaxShow) {
694
- return this.visibleHoverBtns.slice(this.hoverBtnsMaxShow - 1)
695
- }
696
- return []
697
- },
698
-
699
- // 悬浮按钮组样式
700
- hoverBtnsStyle() {
701
- return {
702
- top: `${this.hoverBtnsPosition.top}px`,
703
- right: 0, // 固定在右侧设置列按钮左边
704
- height: `${this.hoverBtnsPosition.height}px`
705
- }
706
- },
707
- // 计算列宽
708
- clacStaticColumnWidth() {
709
- const hasExpand = this.$attrs.treeConfig || this.$attrs['tree-config']
710
- const activeCount = [this.showColumn, this.showSetsize, hasExpand].filter(Boolean).length
711
- return activeCount === 3 ? 112 : 76
712
- },
713
- // 骨架屏定位样式,根据表头实际高度动态计算
714
- skeletonStyle() {
715
- return {
716
- top: `${this.headerHeight}px`,
717
- height: `calc(100% - ${this.headerHeight}px)`
718
- }
719
- }
720
- },
721
- watch: {
722
- columns() {
723
- this.colsKey = this.colsKey + 1
724
- // 没有 pageId 时,columns 变更同步更新 checkColumns
725
- if (!this.pageId) {
726
- this.checkColumns = this.filterDefaultHidden(this.columns)
727
- }
728
- },
729
- size(val) {
730
- this.sizeC = val
731
- },
732
- data() {
733
- // 翻页清除选中
734
- if (this.clearSelect) {
735
- this.$refs.vxeTable.clearCheckboxRow()
736
- this.$emit('selection-change-method', [])
737
- }
738
- },
739
- loading(val) {
740
- if (val) {
741
- this.$nextTick(() => this.updateHeaderHeight())
742
- }
743
- }
744
- },
745
- activated() {
746
- this.$refs.vxeTable.loadData(this.data)
747
- if (this.showColumn && this.pageId) {
748
- this.getColumns()
749
- }
750
- },
751
- beforeDestroy() {
752
- // 清理定时器
753
- this.clearHoverTimers()
754
- },
755
- mounted() {
756
- if (this.showColumn && this.pageId) {
757
- this.getColumns()
758
- }
759
- },
760
- methods: {
761
- handleVisibleColumn() {
762
- this.isExport = false
763
- this.dialogVisible = !this.dialogVisible
764
- },
765
- // 更新表头高度,用于骨架屏定位
766
- updateHeaderHeight() {
767
- const tableEl = this.$refs.vxeTable?.$el
768
- if (!tableEl) return
769
- const headerEl = tableEl.querySelector('.vxe-table--header-wrapper')
770
- if (headerEl) {
771
- this.headerHeight = headerEl.offsetHeight
772
- }
773
- },
774
- openExportColumn() {
775
- this.dialogVisible = true
776
- this.isExport = true
777
- },
778
- resizableChange({ resizeWidth, column, columnIndex }) {
779
- // 通过 prop 匹配 checkColumns 中的列,避免 _columns 过滤后索引错位
780
- const targetCol = column?.property
781
- ? this.checkColumns.find((c) => c.prop === column.property)
782
- : this.checkColumns[columnIndex]
783
- if (targetCol) {
784
- targetCol.width = resizeWidth
785
- }
786
- if (this.showColumn && this.pageId) {
787
- this.saveColumns()
788
- }
789
- },
790
- // 过滤 defaultHidden 列:仅在用户未设置过显示列时作为 fallback 使用
791
- filterDefaultHidden(columns) {
792
- return columns.filter((col) => !col.defaultHidden)
793
- },
794
- // 保存列配置到后端(固定列、列宽调整等场景复用)
795
- saveColumns() {
796
- const userNo = sessionStorage.getItem('userNo')
797
- // 排除模板已固定渲染的操作列,避免保存重复数据
798
- const cols =
799
- this.isOperateFixed && this.btnList && this.btnList.length > 0
800
- ? this.checkColumns.filter((col) => !(col.static && col.label === this.$lc('操作')))
801
- : this.checkColumns
802
- const columns = saveTransform(cols, this.labelKey, this.isFilter)
803
- axios.post(
804
- `/bems/prod_1.0/user/pageHabit?t=${Math.random()}`,
805
- {
806
- userNo,
807
- pageId: this.pageId,
808
- showStructure: JSON.stringify(columns)
809
- },
810
- { loading: false }
811
- )
812
- },
813
- setColumns(list) {
814
- if (this.isExport) {
815
- this.exportFn(list)
816
- this.isExport = false
817
- } else {
818
- this.checkColumns = list
819
- this.colsKey++
820
- }
821
- },
822
- async getColumns() {
823
- try {
824
- const columns = await this.$refs?.showColumn?.getColumns(this.pageId)
825
-
826
- if (columns?.length) {
827
- this.checkColumns = columns
828
- // remote: true 的远程列同步写入 this.columns,保持位置一致
829
- this.syncRemoteColumns(columns)
830
- // 强制 vxe-table 重建,确保固定列图标、颜色等表头状态正确回显
831
- this.colsKey++
832
- } else {
833
- this.checkColumns = this.filterDefaultHidden(this.columns)
834
- }
835
- } catch (err) {
836
- // API 请求失败时回退到过滤后的 columns prop
837
- this.checkColumns = this.filterDefaultHidden(this.columns)
838
- }
839
- },
840
- // 供组件外部调用,刷新表头数据(重新请求 getColumns 并同步远程列)
841
- async refreshColumns() {
842
- if (this.showColumn && this.pageId) {
843
- await this.getColumns()
844
- }
845
- },
846
- // API 返回中标记 remote: true 的列同步到 this.columns,保持对应位置一致
847
- syncRemoteColumns(apiColumns) {
848
- apiColumns.forEach((apiCol, index) => {
849
- if (!apiCol.remote) return
850
- // prop labelKey 匹配 this.columns 中是否已存在
851
- const matchKey = apiCol.prop ? 'prop' : this.labelKey
852
- const existingIdx = this.columns.findIndex((c) => c[matchKey] === apiCol[matchKey])
853
- if (existingIdx !== -1) {
854
- // 已存在则替换,保持位置以 API 返回为准
855
- this.$set(this.columns, existingIdx, apiCol)
856
- } else {
857
- // 不存在则插入到对应位置(避免 $set 索引超出长度产生稀疏数组)
858
- const insertIdx = Math.min(index, this.columns.length)
859
- this.columns.splice(insertIdx, 0, apiCol)
860
- }
861
- })
862
- },
863
- /**
864
- * 一键展开/折叠表格树形行(仅处理第一层级数据行)
865
- * 通过 vxe-table 的 setTreeExpand API 批量设置顶层行的展开状态,
866
- * await Promise 完成后再同步 isExpand 状态,保证图标与实际展开状态一致
867
- * 不直接操作 DOM,不影响排序、筛选等原有逻辑
868
- * @returns {Promise<void>}
869
- */
870
- async toggleExpand() {
871
- const $table = this.$refs.vxeTable
872
- if (!$table) return
873
- // 仅取顶层数据行(data prop 中的第一层,不包含子级)
874
- const topLevelRows = this.data || []
875
- if (topLevelRows.length === 0) return
876
- // 切换展开状态:当前已展开则折叠,当前已折叠则展开
877
- const nextExpand = !this.isExpand
878
- // setTreeExpand 返回 Promise,await 确保展开动作完成后再更新图标状态
879
- await $table.setTreeExpand(topLevelRows, nextExpand)
880
- this.isExpand = nextExpand
881
- },
882
- /**
883
- * 同步单行手动展开/折叠后的图标状态
884
- * 监听 vxe-table toggle-tree-expand 事件,根据当前所有顶层行展开情况更新 isExpand
885
- * @param {Object} expandRecords - vxe-table 当前所有已展开的行记录
886
- * @returns {void}
887
- */
888
- syncExpandState({ expandRecords }) {
889
- const topLevelRows = this.data || []
890
- if (topLevelRows.length === 0) return
891
- // 若顶层行全部处于展开状态则认为是"已展开",否则为"未全部展开"
892
- const expandSet = new Set(expandRecords)
893
- this.isExpand = topLevelRows.every((row) => expandSet.has(row))
894
- },
895
- // 锁定列:将该列 fixed 设为 'left',直接修改 prop 对象并刷新表格
896
- lockColumn(item) {
897
- if (item.fixed || item.static) return
898
- this.$set(item, 'fixed', 'left')
899
- // 手动触发 colsKey 刷新,让 vxe-table 重新渲染列配置
900
- this.colsKey = this.colsKey + 1
901
- // 固定列后自动保存到后端
902
- if (this.showColumn && this.pageId) {
903
- this.saveColumns()
904
- }
905
- },
906
- // 解锁列:移除 fixed 属性,将该列恢复为普通列
907
- unlockColumn(item) {
908
- if (item.fixed !== 'left') return
909
- this.$delete(item, 'fixed')
910
- // 手动触发 colsKey 刷新,让 vxe-table 重新渲染列配置
911
- this.colsKey = this.colsKey + 1
912
- // 取消固定后自动保存到后端
913
- if (this.showColumn && this.pageId) {
914
- this.saveColumns()
915
- }
916
- },
917
- // 固定操作列为内联列
918
- fixOperateColumn() {
919
- this.isOperateFixed = true
920
- this.hideHoverBtns()
921
- this.colsKey = this.colsKey + 1
922
- this.$emit('operate-fixed-change', true)
923
- },
924
- // 取消固定,切换回悬浮按钮组
925
- unfixOperateColumn() {
926
- this.isOperateFixed = false
927
- this.colsKey = this.colsKey + 1
928
- this.$emit('operate-fixed-change', false)
929
- },
930
- // 处理固定操作列的按钮事件
931
- handleOperateCommand(command, row, rowIndex) {
932
- this.$emit(command, row, rowIndex)
933
- //
934
- const btn = this.btnList.find((item) => item.command === command || item.label === command) || { command }
935
- this.$emit('hover-btn-click', { btn, row, rowIndex })
936
- },
937
- // 处理 tableOperate 固定/取消固定切换
938
- handleToggleOperateFixed(fixed) {
939
- if (fixed) {
940
- this.fixOperateColumn()
941
- } else {
942
- this.unfixOperateColumn()
943
- }
944
- },
945
- // 判断按鈕是否显示
946
- hasBtn(isHas, row) {
947
- if (isHas === undefined || isHas === null) {
948
- return true
949
- } else if (typeof isHas === 'boolean') {
950
- return isHas
951
- } else if (typeof isHas === 'string' || Array.isArray(isHas)) {
952
- return this.$has(isHas)
953
- } else if (typeof isHas === 'function') {
954
- return isHas(row)
955
- }
956
- },
957
- // 解析 badge 值,支持 number、string、function
958
- resolveBadge(btn) {
959
- if (!btn.badge) return null
960
- if (typeof btn.badge === 'function') {
961
- const val = btn.badge(this.hoverRowData)
962
- return val || val === 0 ? val : null
963
- }
964
- return btn.badge
965
- },
966
- // 解析 disabled 值,支持 boolean function
967
- resolveDisabled(btn) {
968
- if (btn.disabled === undefined || btn.disabled === null) {
969
- return false
970
- } else if (typeof btn.disabled === 'boolean') {
971
- return btn.disabled
972
- } else if (typeof btn.disabled === 'function') {
973
- return btn.disabled(this.hoverRowData)
974
- }
975
- return false
976
- },
977
- // 全选/反选
978
- toggleAllSelection() {
979
- if (!this.$refs.vxeTable) return
980
- const allData = this.data || []
981
- // 获取合并行索引集合
982
- const mergedIndexes =
983
- this.excludeMergedRows && this.mergeCells && this.mergeCells.length > 0 ? this.getMergedRowIndexes() : new Set()
984
- // 筛选出可选择的行(排除合并从属行 + 排除禁止选择的行)
985
- const selectableRows = allData.filter((row, index) => {
986
- if (mergedIndexes.has(index)) return false
987
- if (typeof this.forbidSelect === 'function' && this.forbidSelect({ row }) === true) return false
988
- return true
989
- })
990
- // 判断当前可选行是否已全部选中
991
- const checkedRows = this.$refs.vxeTable.getCheckboxRecords(false)
992
- const checkedSet = new Set(checkedRows)
993
- const isAllChecked = selectableRows.length > 0 && selectableRows.every((row) => checkedSet.has(row))
994
- if (isAllChecked) {
995
- // 已全选,则取消可选行的选中状态
996
- selectableRows.forEach((row) => {
997
- this.$refs.vxeTable.setCheckboxRow(row, false)
998
- })
999
- } else {
1000
- // 未全选,则选中所有可选行
1001
- selectableRows.forEach((row) => {
1002
- this.$refs.vxeTable.setCheckboxRow(row, true)
1003
- })
1004
- }
1005
- // 得手动触发
1006
- this.handleSelectionChange()
1007
- },
1008
- // 获取被合并的从属行索引集合
1009
- getMergedRowIndexes() {
1010
- const mergedIndexes = new Set()
1011
- if (this.mergeCells && this.mergeCells.length > 0) {
1012
- this.mergeCells.forEach((cell) => {
1013
- if (cell.rowspan > 1) {
1014
- // row+1 row+rowspan-1 都是被合并的从属行
1015
- for (let i = 1; i < cell.rowspan; i++) {
1016
- mergedIndexes.add(cell.row + i)
1017
- }
1018
- }
1019
- })
1020
- }
1021
- return mergedIndexes
1022
- },
1023
-
1024
- // 清空选择
1025
- clearSelection() {
1026
- if (this.$refs.vxeTable) {
1027
- this.$refs.vxeTable.setAllCheckboxRow(false)
1028
- // 得手动触发
1029
- this.handleSelectionChange()
1030
- }
1031
- },
1032
- // 选中某些行
1033
- toggleRowSelection(row, state = true) {
1034
- this.$refs.vxeTable.setCheckboxRow(row, state)
1035
- // 得手动触发
1036
- this.handleSelectionChange()
1037
- },
1038
- toggleAll($table, disabled) {
1039
- if (disabled) {
1040
- return false
1041
- }
1042
- // 如果配置了排除合并行,则手动处理非合并行的选中状态
1043
- if (this.excludeMergedRows && this.mergeCells && this.mergeCells.length > 0) {
1044
- const mergedIndexes = this.getMergedRowIndexes()
1045
- const allData = this.data || []
1046
- // 筛选出可选择的行(排除合并从属行 + 排除禁止选择的行)
1047
- const selectableRows = allData.filter((row, index) => {
1048
- if (mergedIndexes.has(index)) return false
1049
- if (typeof this.forbidSelect === 'function' && this.forbidSelect({ row }) === true) return false
1050
- return true
1051
- })
1052
- // 判断当前可选行是否已全部选中
1053
- const checkedRows = $table.getCheckboxRecords(false)
1054
- const checkedSet = new Set(checkedRows)
1055
- const isAllChecked = selectableRows.length > 0 && selectableRows.every((row) => checkedSet.has(row))
1056
- if (isAllChecked) {
1057
- // 已全选,则取消可选行的选中状态
1058
- selectableRows.forEach((row) => {
1059
- $table.setCheckboxRow(row, false)
1060
- })
1061
- } else {
1062
- // 未全选,则选中所有可选行
1063
- selectableRows.forEach((row) => {
1064
- $table.setCheckboxRow(row, true)
1065
- })
1066
- }
1067
- } else {
1068
- // 默认行为:全选/取消全选所有行
1069
- $table.toggleAllCheckboxRow()
1070
- }
1071
- this.key++
1072
- this.handleSelectionChange()
1073
- },
1074
- toggleChecked($table, row, disabled) {
1075
- if (disabled) {
1076
- return false
1077
- }
1078
- $table.toggleCheckboxRow(row)
1079
- this.key++
1080
- this.handleSelectionChange(row)
1081
- },
1082
- customSortMethod({ sortList }) {
1083
- const orders = sortList.map((item) => {
1084
- return {
1085
- column: item.field,
1086
- asc: item.order === 'asc'
1087
- }
1088
- })
1089
- this.$emit('sort-change-method', orders)
1090
- },
1091
- filterChange(data) {
1092
- const { filterList } = data
1093
- // 输出全部条件
1094
- const obj = {}
1095
- // 复制默认筛选条件为空
1096
- this.checkColumns.forEach((item) => {
1097
- if (item.filters) {
1098
- this.$set(obj, item.prop, undefined)
1099
- }
1100
- })
1101
- filterList.forEach((item) => {
1102
- if (item.column.filterMultiple) {
1103
- this.$set(obj, item.field, item.values)
1104
- } else {
1105
- this.$set(obj, item.field, item.values && item.values[0])
1106
- }
1107
- })
1108
- // 输出单个条件
1109
- const singleObj = {}
1110
- for (let key in obj) {
1111
- if (key === data.field) {
1112
- singleObj[data.field] = obj[data.field]
1113
- }
1114
- }
1115
- if (this.isFiliterSingle) {
1116
- this.$emit('filter-change-method', singleObj)
1117
- } else {
1118
- this.$emit('filter-change-method', obj)
1119
- }
1120
- },
1121
- // row当前单次勾选的哪一行数据 包含checked字段
1122
- handleSelectionChange(row = '') {
1123
- const val = this.$refs.vxeTable.getCheckboxRecords(false)
1124
- // 支持跨页勾选
1125
- const val1 = this.$refs.vxeTable.getCheckboxReserveRecords()
1126
-
1127
- this.$emit('selection-change-method', [...val, ...val1], row)
1128
- },
1129
- // 点击行勾选/取消勾选
1130
- handleCellClick({ row, column, $event }) {
1131
- this.$emit('cell-click', { row, column, $event })
1132
- // 如果点击的是 checkbox 列,不处理(由原生的 checkbox 处理)
1133
- if (column.type === 'checkbox') {
1134
- return
1135
- }
1136
-
1137
- // 检查点击的目标元素是否是按钮或链接
1138
- // 需要检测多种按钮类型:
1139
- // - button: 原生按钮标签
1140
- // - .el-button: Element UI 按钮组件
1141
- // - .n20-button: n20 项目按钮组件
1142
- // - [class*="button"]: 包含 button 类名的元素
1143
- // - [class*="el-link--inner"]: Element UI 链接组件内部元素
1144
- // - a[href]: 链接标签
1145
- const target = $event.target
1146
- const clickedButton = target.closest(
1147
- 'button, .el-button, .n20-button, [class*="button"], [class*="el-link--inner"], a[href]'
1148
- )
1149
-
1150
- // 如果点击的是按钮或链接,不触发行勾选
1151
- if (clickedButton) {
1152
- return
1153
- }
1154
-
1155
- // 如果 closest 没找到,检查父元素的类名是否包含操作相关的关键词
1156
- if (!clickedButton && target.parentElement) {
1157
- const parentClass = target.parentElement.className || ''
1158
- // 检测父元素是否包含:operate(操作)、btn(按钮)、button(按钮) 等关键词
1159
- if (parentClass.includes('operate') || parentClass.includes('btn') || parentClass.includes('button')) {
1160
- return
1161
- }
1162
- }
1163
-
1164
- // 检查该行是否可以被勾选
1165
- let canCheck = true
1166
- if (typeof this.forbidSelect === 'function') {
1167
- const forbidResult = this.forbidSelect({ row })
1168
- // 只有当 forbidSelect 明确返回 true 时,才允许勾选
1169
- canCheck = forbidResult !== false
1170
- }
1171
-
1172
- if (canCheck) {
1173
- // 切换该行的勾选状态
1174
- this.$refs.vxeTable.toggleCheckboxRow(row)
1175
- this.key++ // 强制刷新 checkbox 显示
1176
- this.handleSelectionChange(row)
1177
- }
1178
- },
1179
- sizeUp(size) {
1180
- this.sizeC = size
1181
- this.$emit('update:size', size)
1182
- },
1183
- sizeSet(el) {
1184
- this.sizeBind = el
1185
- },
1186
- // 计算列宽
1187
- // ========== 悬浮按钮组相关方法 ==========
1188
- // 清理所有定时器
1189
- clearHoverTimers() {
1190
- if (this.hoverShowTimer) {
1191
- clearTimeout(this.hoverShowTimer)
1192
- this.hoverShowTimer = null
1193
- }
1194
- if (this.hoverHideTimer) {
1195
- clearTimeout(this.hoverHideTimer)
1196
- this.hoverHideTimer = null
1197
- }
1198
- },
1199
- // 鼠标进入单元格事件(防抖处理)
1200
- handleCellMouseEnter({ row, rowIndex, $rowIndex, $event }) {
1201
- // 如果没有配置悬浮按钮,或已固定为列模式,不处理
1202
- if (!this.btnList || this.btnList.length === 0 || this.isOperateFixed) {
1203
- return
1204
- }
1205
- // 清理隐藏定时器
1206
- if (this.hoverHideTimer) {
1207
- clearTimeout(this.hoverHideTimer)
1208
- this.hoverHideTimer = null
1209
- }
1210
- // 如果是同一行,不重复处理
1211
- if (this.hoverRowIndex === rowIndex && this.showHoverBtns) {
1212
- return
1213
- }
1214
- // 清理之前的显示定时器
1215
- if (this.hoverShowTimer) {
1216
- clearTimeout(this.hoverShowTimer)
1217
- }
1218
- // 延迟显示(防抖)
1219
- this.hoverShowTimer = setTimeout(() => {
1220
- this.updateHoverRow(row, rowIndex, $event)
1221
- }, this.hoverBtnsDelay)
1222
- },
1223
- // 鼠标离开单元格事件(防抖处理)
1224
- handleCellMouseLeave({ row, rowIndex, $event }) {
1225
- // 如果没有配置悬浮按钮,或已固定为列模式,不处理
1226
- if (!this.btnList || this.btnList.length === 0 || this.isOperateFixed) {
1227
- return
1228
- }
1229
- // 清理显示定时器
1230
- if (this.hoverShowTimer) {
1231
- clearTimeout(this.hoverShowTimer)
1232
- this.hoverShowTimer = null
1233
- }
1234
- // 延迟隐藏(给鼠标移动到按钮组的时间)
1235
- this.hoverHideTimer = setTimeout(() => {
1236
- if (!this.isHoverOnBtnGroup) {
1237
- this.hideHoverBtns()
1238
- }
1239
- }, this.hoverBtnsHideDelay)
1240
- },
1241
- // 更新悬停行信息并显示按钮组
1242
- updateHoverRow(row, rowIndex, $event) {
1243
- this.hoverRowData = row
1244
- this.hoverRowIndex = rowIndex
1245
- // 计算按钮组位置
1246
- this.calculateBtnPosition($event)
1247
- // 显示按钮组
1248
- this.showHoverBtns = true
1249
- // 触发事件
1250
- this.$emit('row-hover-enter', { row, rowIndex })
1251
- },
1252
- // 计算按钮组位置
1253
- calculateBtnPosition($event) {
1254
- const tableEl = this.$refs.vxeTable?.$el
1255
- if (!tableEl) return
1256
- // 获取当前行的 DOM 元素
1257
- const rowEl = $event?.target?.closest('tr.vxe-body--row')
1258
- if (!rowEl) return
1259
- // 获取表格容器的位置信息
1260
- const tableRect = tableEl.getBoundingClientRect()
1261
- const rowRect = rowEl.getBoundingClientRect()
1262
- // 计算相对于表格容器的位置
1263
- this.hoverBtnsPosition = {
1264
- top: rowRect.top - tableRect.top,
1265
- height: rowRect.height
1266
- }
1267
- },
1268
- // 隐藏悬浮按钮组
1269
- hideHoverBtns() {
1270
- // 如果下拉菜单正在展开,不隐藏
1271
- if (this.isDropdownVisible) {
1272
- return
1273
- }
1274
- // 移除当前行的 hover 类名
1275
- this.removeRowHoverClass()
1276
- this.showHoverBtns = false
1277
- this.hoverRowData = null
1278
- this.hoverRowIndex = -1
1279
- this.isHoverOnBtnGroup = false
1280
- // 触发事件
1281
- this.$emit('row-hover-leave')
1282
- },
1283
- // 鼠标进入按钮组
1284
- handleBtnGroupEnter() {
1285
- this.isHoverOnBtnGroup = true
1286
- // 清理隐藏定时器
1287
- if (this.hoverHideTimer) {
1288
- clearTimeout(this.hoverHideTimer)
1289
- this.hoverHideTimer = null
1290
- }
1291
- // 给当前行添加 hover 类名
1292
- this.addRowHoverClass()
1293
- },
1294
- // 鼠标离开按钮组
1295
- handleBtnGroupLeave() {
1296
- this.isHoverOnBtnGroup = false
1297
- // 如果下拉菜单正在展开,不隐藏按钮组
1298
- if (this.isDropdownVisible) {
1299
- return
1300
- }
1301
- // 移除当前行的 hover 类名
1302
- this.removeRowHoverClass()
1303
- // 延迟隐藏
1304
- this.hoverHideTimer = setTimeout(() => {
1305
- // 再次检查下拉菜单状态
1306
- if (!this.isDropdownVisible) {
1307
- this.hideHoverBtns()
1308
- }
1309
- }, this.hoverBtnsHideDelay)
1310
- },
1311
- // 处理下拉菜单显示/隐藏状态变化
1312
- handleDropdownVisibleChange(visible) {
1313
- this.isDropdownVisible = visible
1314
- if (!visible) {
1315
- // 下拉菜单关闭后,延迟检查是否需要隐藏按钮组
1316
- this.hoverHideTimer = setTimeout(() => {
1317
- if (!this.isHoverOnBtnGroup && !this.isDropdownVisible) {
1318
- this.hideHoverBtns()
1319
- }
1320
- }, this.hoverBtnsHideDelay)
1321
- }
1322
- },
1323
- // 给当前悬停行添加 hover 类名
1324
- addRowHoverClass() {
1325
- const tableEl = this.$refs.vxeTable?.$el
1326
- if (!tableEl || this.hoverRowIndex < 0) return
1327
- // 查找当前行的 tr 元素
1328
- const rows = tableEl.querySelectorAll('tr.vxe-body--row')
1329
- rows.forEach((row, index) => {
1330
- if (index === this.hoverRowIndex) {
1331
- row.classList.add('row--hover')
1332
- }
1333
- })
1334
- },
1335
- // 移除当前悬停行的 hover 类名
1336
- removeRowHoverClass() {
1337
- const tableEl = this.$refs.vxeTable?.$el
1338
- if (!tableEl) return
1339
- // 移除所有行的 hover 类名
1340
- const rows = tableEl.querySelectorAll('tr.vxe-body--row.row--hover')
1341
- rows.forEach((row) => {
1342
- row.classList.remove('row--hover')
1343
- })
1344
- },
1345
- // 处理悬浮按钮点击
1346
- handleHoverBtnClick(btn, $event) {
1347
- if (this.resolveDisabled(btn)) return
1348
-
1349
- if (typeof btn.command === 'function') {
1350
- btn.command(this.hoverRowData, this.hoverRowIndex, $event)
1351
- } else if (typeof btn.command === 'string') {
1352
- this.$emit(btn.command, this.hoverRowData, this.hoverRowIndex, $event)
1353
- }
1354
- // 触发事件
1355
- this.$emit('hover-btn-click', {
1356
- btn,
1357
- row: this.hoverRowData,
1358
- rowIndex: this.hoverRowIndex,
1359
- $event
1360
- })
1361
- },
1362
- // 处理"更多"下拉菜单命令
1363
- handleMoreBtnCommand(btn) {
1364
- if (!btn || this.resolveDisabled(btn)) return
1365
- this.handleHoverBtnClick(btn, null)
1366
- },
1367
- // 表格滚动时隐藏按钮组
1368
- handleTableScroll() {
1369
- if (this.showHoverBtns) {
1370
- this.hideHoverBtns()
1371
- }
1372
- },
1373
- // ========== 列宽计算相关方法 ==========
1374
- getRowClassName(args) {
1375
- const rowClassName = this.$attrs['row-class-name'] || this.$attrs.rowClassName
1376
- // 用户传入了 row-class-name,以传入的为准
1377
- if (rowClassName && typeof rowClassName === 'function') {
1378
- return rowClassName(args)
1379
- }
1380
- if (rowClassName && typeof rowClassName === 'string') {
1381
- return rowClassName
1382
- }
1383
- // 用户未传入,选中行高亮
1384
- const checkedRows = this.$refs.vxeTable?.getCheckboxRecords(false) || []
1385
- const checkedSet = new Set(checkedRows)
1386
- return checkedSet.has(args.row) ? 'v3-n20-table-pro__row-checked' : ''
1387
- },
1388
-
1389
- calcColumnWidth(columns) {
1390
- // 常量配置
1391
- const CHAR_WIDTH = 20 // 每个字符的平均宽度(像素)
1392
- const PADDING = 20 // 左右padding
1393
- const CHECKBOX_WIDTH = 50 // 复选框列宽度
1394
- const OPERATE_WIDTH = 180 // 操作列宽度
1395
- const HOVER_BTNS_WIDTH = 356 // 悬浮按钮组宽度
1396
- const MIN_LABEL_LENGTH = 10 // 最小标签长度
1397
-
1398
- // 解析宽度值(支持数字、"100"、"100px"格式)
1399
- const parseWidth = (value) => (typeof value === 'number' ? value : parseInt(value, 10) || 0)
1400
-
1401
- const calcColumns = columns.map((column) => {
1402
- // 操作列固定宽度
1403
- if (column.static && column.label === this.$lc('操作') && !column.width && !column.minWidth) {
1404
- column.width = OPERATE_WIDTH
1405
- }
1406
- // 复选框列固定宽度
1407
- if (column.type === 'checkbox') {
1408
- column.width = CHECKBOX_WIDTH
1409
- }
1410
- // 未设置宽度时,根据标签字符数计算最小宽度
1411
- if (!column.width && !column.minWidth && !column['min-width']) {
1412
- const textLength = column.label?.length || MIN_LABEL_LENGTH
1413
- column.minWidth = Math.ceil(textLength * CHAR_WIDTH + PADDING)
1414
- }
1415
- // 计算基础宽度
1416
- const widthValue = column.width || column.minWidth || column['min-width'] || 0
1417
- const baseWidth = parseWidth(widthValue)
1418
- column._baseWidth_ = baseWidth
1419
- return column
1420
- })
1421
-
1422
- // 悬浮按钮:给最后一列预留按钮组空间(固定模式下不需要)
1423
- if (!this.isOperateFixed && this.btnList?.length && calcColumns.length > 0) {
1424
- const lastColumn = calcColumns[calcColumns.length - 1]
1425
- const baseWidth = lastColumn._baseWidth_ || 0
1426
- if (baseWidth <= HOVER_BTNS_WIDTH) {
1427
- lastColumn.width = HOVER_BTNS_WIDTH + baseWidth
1428
- }
1429
- lastColumn.align = 'left'
1430
- }
1431
- return calcColumns
1432
- }
1433
- }
1434
- }
1435
- </script>
1
+ <template>
2
+ <div class="v3-table-pro-wrapper" style="height: 100%; position: relative; overflow: hidden">
3
+ <!-- 骨架屏 - 仅覆盖内容区域,不覆盖表头 -->
4
+ <div v-if="loading" class="table-pro-skeleton" :style="skeletonStyle">
5
+ <div v-for="n in skeletonRows" :key="'skeleton-' + n" class="skeleton-row">
6
+ <div class="skeleton-cell skeleton-checkbox"></div>
7
+ <div v-for="col in skeletonCols" :key="'col-' + col" class="skeleton-cell"></div>
8
+ </div>
9
+ </div>
10
+ <!-- 表格 -->
11
+ <vxe-table
12
+ ref="vxeTable"
13
+ :key="colsKey"
14
+ :align="'center'"
15
+ :data="data"
16
+ :height="height"
17
+ :class="[{ 'cell-default-set--': cellDefault }, 'v3-n20-table-pro']"
18
+ :checkbox-config="{
19
+ checkField: 'checked',
20
+ checkMethod: forbidSelect,
21
+ ...$attrs['checkbox-config'],
22
+ ...$attrs.checkboxConfig
23
+ }"
24
+ show-header-overflow
25
+ show-overflow
26
+ :row-config="{
27
+ isHover: true,
28
+ useKey: true,
29
+ ...$attrs.rowConfig,
30
+ ...$attrs['row-config']
31
+ }"
32
+ :column-config="{
33
+ resizable: true,
34
+ useKey: true,
35
+ ...$attrs.columnConfig,
36
+ ...$attrs['column-config']
37
+ }"
38
+ :virtual-y-config="scrollY"
39
+ :sort-config="{
40
+ multiple: false,
41
+ remote: true,
42
+ trigger: 'cell',
43
+ ...$attrs.sortConfig,
44
+ ...$attrs['sort-config']
45
+ }"
46
+ :filter-config="{
47
+ remote: true,
48
+ iconNone: 'n20-icon-xiala-moren',
49
+ iconMatch: 'n20-icon-xiala-moren'
50
+ }"
51
+ :merge-cells="mergeCells"
52
+ :row-class-name="getRowClassName"
53
+ v-bind="{ size, ...$attrs, ...sizeBind }"
54
+ v-on="$listeners"
55
+ @sort-change="(val) => customSortMethod(val)"
56
+ @filter-change="filterChange"
57
+ @checkbox-change="handleSelectionChange"
58
+ @cell-click="handleCellClick"
59
+ @cell-mouseenter="handleCellMouseEnter"
60
+ @cell-mouseleave="handleCellMouseLeave"
61
+ @scroll="handleTableScroll"
62
+ @toggle-tree-expand="syncExpandState"
63
+ @column-resizable-change="resizableChange"
64
+ >
65
+ <template v-for="(item, i) in _columns">
66
+ <slot v-if="item.slotName" :name="item.slotName" :column="item"></slot>
67
+ <vxe-column
68
+ v-else-if="item.render"
69
+ :key="'vxe-table__render_' + i"
70
+ v-bind="{ headerAlign: 'center', ...item }"
71
+ :formatter="item.formatter ? item.formatter : 'formatName'"
72
+ :filters="item.filters"
73
+ :filter-render="item.filters && item.filters.length > 0 ? { name: 'FilterInput' } : null"
74
+ :title="item.label"
75
+ :field="item.prop"
76
+ >
77
+ <template #header="{ column }">
78
+ <div
79
+ class="flex-box flex-v flex-c"
80
+ @mouseenter="hoverHeaderProp = item.prop"
81
+ @mouseleave="hoverHeaderProp = null"
82
+ >
83
+ <slot name="header" :column="column">{{ column.title }}</slot>
84
+ <i
85
+ v-if="item.tooltip"
86
+ v-title="item.tooltip"
87
+ class="n20-icon-xinxitishi vxe-table--column__icon m-l-ss"
88
+ ></i>
89
+ <!-- 已固定列(fixed='left'):hover 时显示 lock 图标,点击取消固定 -->
90
+ <i
91
+ v-if="item.fixed === 'left'"
92
+ class="v3-icon-lock vxe-table--column__icon m-l-ss pointer color-primary"
93
+ @click.stop="unlockColumn(item)"
94
+ ></i>
95
+ <!-- 未固定列(且非静态列):hover 时显示 unlock 图标,点击设为固定 -->
96
+ <i
97
+ v-if="!item.fixed && !item.static"
98
+ v-show="hoverHeaderProp === item.prop"
99
+ class="v3-icon-unlock vxe-table--column__icon m-l-ss pointer"
100
+ @click.stop="lockColumn(item)"
101
+ ></i>
102
+ </div>
103
+ </template>
104
+ <template slot-scope="scope">
105
+ <renderer :render-content="item.render" :scope="scope" />
106
+ </template>
107
+ </vxe-column>
108
+ <vxe-column
109
+ v-else-if="item.type === 'radio'"
110
+ :key="'vxe-table-radio-' + i"
111
+ v-bind="item"
112
+ :title="item.label"
113
+ :field="item.prop"
114
+ type="radio"
115
+ :width="item.width || 50"
116
+ :fixed="item.fixed"
117
+ />
118
+ <vxe-column
119
+ v-else-if="item.type === 'checkbox'"
120
+ :key="'vxe-table-' + i"
121
+ v-bind="item"
122
+ :title="item.label"
123
+ :field="item.prop"
124
+ type="checkbox"
125
+ width="50"
126
+ :fixed="item.fixed || item.fixed === '' ? item.fixed : 'left'"
127
+ >
128
+ <template #header="{ $table, checked, indeterminate, disabled }">
129
+ <span class="custom-checkbox">
130
+ <el-checkbox
131
+ v-if="indeterminate"
132
+ :indeterminate="true"
133
+ :disabled="disabled"
134
+ @click.native.stop.prevent="toggleAll($table, disabled, $event)"
135
+ />
136
+ <el-checkbox
137
+ v-else
138
+ :value="data && data.length > 0 ? checked : false"
139
+ :disabled="disabled"
140
+ @click.native.stop.prevent="toggleAll($table, disabled, $event)"
141
+ />
142
+ </span>
143
+ </template>
144
+ <template #checkbox="{ $table, row, checked, indeterminate, disabled }">
145
+ <span class="custom-checkbox" @click.stop="toggleChecked($table, row, disabled)">
146
+ <el-checkbox
147
+ v-if="indeterminate"
148
+ :key="key + '-row-indeterminate'"
149
+ :indeterminate="indeterminate"
150
+ :disabled="disabled"
151
+ />
152
+ <el-checkbox v-else-if="checked" :key="key + '-row-checked'" :value="checked" :disabled="disabled" />
153
+ <el-checkbox v-else :key="key + '-row-unchecked'" :value="checked" :disabled="disabled" />
154
+ </span>
155
+ </template>
156
+ </vxe-column>
157
+ <vxe-colgroup
158
+ v-else-if="item.children && item.children.length > 0"
159
+ :key="'vxe-colgroup-' + i"
160
+ :title="item.label"
161
+ >
162
+ <vxe-column
163
+ v-for="(subItem, i) in item.children"
164
+ :key="'vxe-table-' + i"
165
+ :formatter="subItem.formatter ? subItem.formatter : 'formatName'"
166
+ :filters="subItem.filters"
167
+ :filter-render="subItem.filterRender"
168
+ :title="subItem.label"
169
+ :field="subItem.prop"
170
+ v-bind="{ headerAlign: 'center', ...subItem }"
171
+ :class-name="`${subItem.wrap && `vxe-table-custom-wrap`} ${subItem.bold && `font-w600`}`"
172
+ />
173
+ </vxe-colgroup>
174
+ <vxe-column
175
+ v-else-if="item.formatter"
176
+ :key="'vxe-table-base__column_formatter' + i"
177
+ :class-name="`${item.wrap && `vxe-table-custom-wrap`} ${item.bold && `font-w600`}`"
178
+ :formatter="item.formatter ? item.formatter : 'formatName'"
179
+ :filters="item.filters"
180
+ :filter-render="item.filters && item.filters.length > 0 ? { name: 'FilterInput' } : null"
181
+ :title="item.label"
182
+ :field="item.prop"
183
+ v-bind="{ headerAlign: 'center', ...item }"
184
+ >
185
+ <template #header="{ column }">
186
+ <div
187
+ class="flex-box flex-v flex-c"
188
+ @mouseenter="hoverHeaderProp = item.prop"
189
+ @mouseleave="hoverHeaderProp = null"
190
+ >
191
+ <span
192
+ v-if="item.tooltip"
193
+ v-title="typeof item.tooltip === 'function' ? item.tooltip(column) : item.tooltip"
194
+ class="pointer"
195
+ style="border-bottom: 1px dashed"
196
+ >
197
+ <slot :name="`header_${column.field}`" :column="column">
198
+ {{ column.title }}
199
+ </slot>
200
+ </span>
201
+ <template v-else>
202
+ <slot :name="`header_${column.field}`" :column="column">
203
+ {{ column.title }}
204
+ </slot>
205
+ </template>
206
+ <!-- 已固定列(fixed='left'):hover 时显示 lock 图标,点击取消固定 -->
207
+ <i
208
+ v-if="item.fixed === 'left'"
209
+ class="v3-icon-lock vxe-table--column__icon m-l-ss pointer color-primary"
210
+ @click.stop="unlockColumn(item)"
211
+ ></i>
212
+ <!-- 未固定列(且非静态列):hover 时显示 unlock 图标,点击设为固定 -->
213
+ <i
214
+ v-if="!item.fixed && !item.static"
215
+ v-show="hoverHeaderProp === item.prop"
216
+ class="v3-icon-unlock vxe-table--column__icon m-l-ss pointer"
217
+ @click.stop="lockColumn(item)"
218
+ ></i>
219
+ </div>
220
+ </template>
221
+ </vxe-column>
222
+ <vxe-column
223
+ v-else
224
+ :key="'vxe-table-base__column' + i"
225
+ :class-name="`${item.wrap && `vxe-table-custom-wrap`} ${item.bold && `font-w600`}`"
226
+ :filters="item.filters"
227
+ :filter-render="item.filters && item.filters.length > 0 ? { name: 'FilterInput' } : null"
228
+ :title="item.label"
229
+ :field="item.prop"
230
+ v-bind="{ headerAlign: 'center', ...item }"
231
+ >
232
+ <template #header="{ column }">
233
+ <div
234
+ class="flex-box flex-v flex-c"
235
+ @mouseenter="hoverHeaderProp = item.prop"
236
+ @mouseleave="hoverHeaderProp = null"
237
+ >
238
+ <span
239
+ v-if="item.tooltip"
240
+ v-title="typeof item.tooltip === 'function' ? item.tooltip(column) : item.tooltip"
241
+ class="pointer"
242
+ style="border-bottom: 1px dashed"
243
+ >
244
+ <slot :name="`header_${column.field}`" :column="column">
245
+ {{ column.title }}
246
+ </slot>
247
+ </span>
248
+ <template v-else>
249
+ <slot :name="`header_${column.field}`" :column="column">
250
+ {{ column.title }}
251
+ </slot>
252
+ </template>
253
+ <!-- 已固定列(fixed='left'):hover 时显示 lock 图标,点击取消固定 -->
254
+ <i
255
+ v-if="item.fixed === 'left'"
256
+ class="v3-icon-lock vxe-table--column__icon m-l-ss pointer color-primary"
257
+ @click.stop="unlockColumn(item)"
258
+ ></i>
259
+ <!-- 未固定列(且非静态列):hover 时显示 unlock 图标,点击设为固定 -->
260
+ <i
261
+ v-if="!item.fixed && !item.static"
262
+ v-show="hoverHeaderProp === item.prop"
263
+ class="v3-icon-unlock vxe-table--column__icon m-l-ss pointer"
264
+ @click.stop="lockColumn(item)"
265
+ ></i>
266
+ </div>
267
+ </template>
268
+ <!-- 处理旧格式 formatter render -->
269
+ <template v-if="item.type !== 'seq'" #default="{ row, column }">
270
+ <!-- 纯对象字段 -->
271
+ <template
272
+ v-if="row[column.property] && typeof row[column.property] === 'object' && row[column.property].name"
273
+ >
274
+ {{ row[column.property].name }}
275
+ </template>
276
+ <slot v-else :name="`cell_${item.prop}`" :row="row">{{
277
+ row[column.property] || row[column.property] === 0 ? row[column.property] : '--'
278
+ }}</slot>
279
+ </template>
280
+ </vxe-column>
281
+ </template>
282
+ <!-- 固定操作列 -->
283
+ <vxe-column
284
+ v-if="isOperateFixed && btnList && btnList.length > 0"
285
+ :title="$lc('操作')"
286
+ field="__operate__"
287
+ :width="operateColumnWidth"
288
+ fixed="right"
289
+ align="center"
290
+ >
291
+ <template #header>
292
+ <div class="flex-box flex-v flex-c">
293
+ {{ $lc('操作') }}
294
+ <i class="v3-icon-unlock vxe-table--column__icon m-l-ss pointer" @click.stop="unfixOperateColumn"></i>
295
+ </div>
296
+ </template>
297
+ <template #default="{ row, $rowIndex }">
298
+ <OperateBtns
299
+ :btn-list="btnList"
300
+ :row="row"
301
+ :get-operate-btns="getOperateBtns"
302
+ @command="(c) => handleOperateCommand(c, row, $rowIndex)"
303
+ />
304
+ </template>
305
+ </vxe-column>
306
+ <template #empty>
307
+ <slot name="empty">
308
+ <empty type="noData" :content="$lc('暂无数据')" :height="100" :width="100" />
309
+ </slot>
310
+ </template>
311
+ </vxe-table>
312
+ <!-- 操作列 -->
313
+ <tableOperate
314
+ :show-column="showColumn"
315
+ :show-setsize="showSetsize"
316
+ :show-pin-btn="btnList && btnList.length > 0"
317
+ :is-operate-fixed="isOperateFixed"
318
+ :size="sizeC"
319
+ v-bind="$attrs"
320
+ @update:size="sizeUp"
321
+ @resize="sizeSet"
322
+ @toggle-expand="toggleExpand"
323
+ @toggle-operate-fixed="handleToggleOperateFixed"
324
+ @visible-column="handleVisibleColumn"
325
+ />
326
+ <!-- 操作列的悬浮按钮组 -->
327
+ <transition name="hover-btns-fade">
328
+ <div
329
+ v-if="showHoverBtns && visibleHoverBtns.length > 0 && !isOperateFixed"
330
+ class="table-hover-btns"
331
+ :style="hoverBtnsStyle"
332
+ @mouseenter="handleBtnGroupEnter"
333
+ @mouseleave="handleBtnGroupLeave"
334
+ >
335
+ <div class="hover-btns-wrapper">
336
+ <template v-for="(btn, idx) in displayHoverBtns">
337
+ <!-- 纯图标按钮:与固定列的 el-link 图标按钮样式一致 -->
338
+ <el-tooltip
339
+ v-if="btn.iconOnly && btn.icon"
340
+ :key="'display-' + idx"
341
+ :content="btn.tip || btn.label"
342
+ placement="top"
343
+ >
344
+ <el-link
345
+ :type="btn.type || 'primary'"
346
+ :disabled="resolveDisabled(btn)"
347
+ :underline="false"
348
+ :class="['hover-btn-icon-only', btn.class]"
349
+ @click.stop="handleHoverBtnClick(btn, $event)"
350
+ >
351
+ <i :class="btn.icon"></i>
352
+ </el-link>
353
+ </el-tooltip>
354
+ <!-- 图标+文字按钮 -->
355
+ <el-button
356
+ v-else-if="btn.icon"
357
+ :key="'display-icon-' + idx"
358
+ :type="btn.type || 'primary'"
359
+ :size="btn.size || 'mini'"
360
+ :disabled="resolveDisabled(btn)"
361
+ :class="[btn.class]"
362
+ @click.stop="handleHoverBtnClick(btn, $event)"
363
+ >
364
+ <i :class="btn.icon" class="m-r-ss"></i>
365
+ <el-badge
366
+ v-if="resolveBadge(btn)"
367
+ :value="resolveBadge(btn)"
368
+ :max="btn.badgeMax || 99"
369
+ class="hover-btn-badge"
370
+ >
371
+ {{ btn.label }}
372
+ </el-badge>
373
+ <template v-else>{{ btn.label }}</template>
374
+ </el-button>
375
+ <!-- 纯文字按钮 -->
376
+ <el-button
377
+ v-else
378
+ :key="'display-text-' + idx"
379
+ :type="btn.type || 'primary'"
380
+ :size="btn.size || 'mini'"
381
+ :disabled="resolveDisabled(btn)"
382
+ :class="[btn.class]"
383
+ @click.stop="handleHoverBtnClick(btn, $event)"
384
+ >
385
+ <el-badge
386
+ v-if="resolveBadge(btn)"
387
+ :value="resolveBadge(btn)"
388
+ :max="btn.badgeMax || 99"
389
+ class="hover-btn-badge"
390
+ >
391
+ {{ btn.label }}
392
+ </el-badge>
393
+ <template v-else>{{ btn.label }}</template>
394
+ </el-button>
395
+ </template>
396
+ <!-- 更多按钮下拉菜单 -->
397
+ <el-dropdown
398
+ v-if="moreHoverBtns.length > 0"
399
+ class="hover-btns-more"
400
+ @command="handleMoreBtnCommand"
401
+ @visible-change="handleDropdownVisibleChange"
402
+ >
403
+ <el-button type="primary" size="mini">
404
+ {{ $lc('更多') }}<i class="el-icon-arrow-down el-icon--right"></i>
405
+ </el-button>
406
+ <el-dropdown-menu slot="dropdown">
407
+ <el-dropdown-item
408
+ v-for="(btn, idx) in moreHoverBtns"
409
+ :key="'more-' + idx"
410
+ :command="btn"
411
+ :disabled="resolveDisabled(btn)"
412
+ >
413
+ <i v-if="btn.icon" :class="btn.icon" class="m-r-ss"></i>
414
+ <el-badge v-if="resolveBadge(btn)" :value="resolveBadge(btn)" :max="btn.badgeMax || 99">
415
+ <span :class="btn.type === 'danger' ? 'color-danger' : ''">
416
+ {{ btn.label }}
417
+ </span>
418
+ </el-badge>
419
+ <span v-else :class="btn.type === 'danger' ? 'color-danger' : ''">
420
+ {{ btn.label }}
421
+ </span>
422
+ </el-dropdown-item>
423
+ </el-dropdown-menu>
424
+ </el-dropdown>
425
+ </div>
426
+ </div>
427
+ </transition>
428
+
429
+ <table-show-column
430
+ ref="showColumn"
431
+ :dialog-visible.sync="dialogVisible"
432
+ :check-columns="checkColumns"
433
+ :columns="columns"
434
+ :label-key="labelKey"
435
+ :auto-save="autoSave"
436
+ :page-id="pageId"
437
+ :is-export="isExport"
438
+ :is-filter="isFilter"
439
+ :width="width"
440
+ :has-p-x="hasPX"
441
+ @setColumns="setColumns"
442
+ />
443
+ </div>
444
+ </template>
445
+
446
+ <script>
447
+ import axios from '../../../utils/axios'
448
+ import empty from '../../Empty'
449
+ import tableShowColumn from '../../ShowColumn/index.vue'
450
+ import { saveTransform } from '../../ShowColumn/index.vue'
451
+ import tableOperate from '../../TableOperate/index.vue'
452
+ import OperateBtns from '../../TableProOperateColumn/OperateBtns.vue'
453
+
454
+ const renderer = {
455
+ name: 'renderer',
456
+ props: {
457
+ renderContent: {
458
+ type: Function
459
+ },
460
+ scope: {
461
+ type: Object
462
+ }
463
+ },
464
+ render(h) {
465
+ return this.renderContent(h, this.scope.row, this.scope)
466
+ }
467
+ }
468
+ export default {
469
+ name: 'TableProV3',
470
+ components: {
471
+ tableOperate,
472
+ tableShowColumn,
473
+ empty,
474
+ renderer,
475
+ OperateBtns
476
+ },
477
+ props: {
478
+ scrollY: {
479
+ type: Object,
480
+ default: () => {
481
+ return { enabled: true, gt: 0, model: 'wheel' }
482
+ }
483
+ },
484
+ data: {
485
+ type: Array,
486
+ default: undefined
487
+ },
488
+ columns: {
489
+ type: Array,
490
+ require: true,
491
+ default: () => []
492
+ },
493
+ height: {
494
+ type: [String, Number],
495
+ default: undefined
496
+ },
497
+ size: {
498
+ type: String,
499
+ default: 'small'
500
+ },
501
+ showSetsize: {
502
+ type: Boolean,
503
+ default: true
504
+ },
505
+ cellDefault: {
506
+ type: Boolean,
507
+ default: true
508
+ },
509
+ // 禁止选择
510
+ forbidSelect: {
511
+ type: Function,
512
+ default: () => {
513
+ const forbidSelect = function ({ row }) {
514
+ return row
515
+ }
516
+ return forbidSelect
517
+ }
518
+ },
519
+ // 是否显示显示列
520
+ showColumn: {
521
+ type: Boolean,
522
+ default: true
523
+ },
524
+ // 数据更新时是否自动清空已选
525
+ clearSelect: {
526
+ type: Boolean,
527
+ default: true
528
+ },
529
+ // 筛选条件只输出当前筛选的单个条件,默认为全部条件
530
+ isFiliterSingle: {
531
+ type: Boolean,
532
+ default: false
533
+ },
534
+ isAutoWidth: {
535
+ type: Boolean,
536
+ default: true
537
+ },
538
+ // 合并单元格配置,格式: [{ row: 0, col: 0, rowspan: 2, colspan: 0 }]
539
+ mergeCells: {
540
+ type: Array,
541
+ default: () => []
542
+ },
543
+ // 全选时是否排除被合并的行(从属行)
544
+ excludeMergedRows: {
545
+ type: Boolean,
546
+ default: false
547
+ },
548
+ // 是否显示骨架屏加载状态
549
+ loading: {
550
+ type: Boolean,
551
+ default: false
552
+ },
553
+ // 骨架屏行数
554
+ skeletonRows: {
555
+ type: Number,
556
+ default: 10
557
+ },
558
+ // 骨架屏列数
559
+ skeletonCols: {
560
+ type: Number,
561
+ default: 6
562
+ },
563
+ // 悬浮按钮列表配置
564
+ // 格式: [{ command: 'edit', label: '编辑', type: 'text', isHas: 'edit', click: (row) => {} }]
565
+ btnList: {
566
+ type: Array,
567
+ default: () => []
568
+ },
569
+ // 权限检查函数,返回当前行允许显示的按钮 key 列表
570
+ // 格式: (row, btns) => ['edit', 'delete'] 或 true(显示所有)或 false(不显示任何)
571
+ hoverBtnsPermission: {
572
+ type: Function,
573
+ default: null
574
+ },
575
+ // 悬浮按钮组是否显示分隔符
576
+ hoverBtnsDivider: {
577
+ type: Boolean,
578
+ default: true
579
+ },
580
+ // 悬浮按钮组显示延迟(毫秒)
581
+ hoverBtnsDelay: {
582
+ type: Number,
583
+ default: 50
584
+ },
585
+ // 悬浮按钮组隐藏延迟(毫秒)
586
+ hoverBtnsHideDelay: {
587
+ type: Number,
588
+ default: 100
589
+ },
590
+ // 悬浮按钮组最大显示数量,超过则收起到"更多"下拉菜单
591
+ hoverBtnsMaxShow: {
592
+ type: Number,
593
+ default: 4
594
+ },
595
+ // 列头标签键
596
+ labelKey: {
597
+ type: String,
598
+ default: 'label'
599
+ },
600
+ // 是否自动保存列设置
601
+ autoSave: {
602
+ type: Boolean,
603
+ default: true
604
+ },
605
+ pageId: {
606
+ type: String,
607
+ default: undefined
608
+ },
609
+ isFilter: {
610
+ type: Boolean,
611
+ default: false
612
+ },
613
+ width: {
614
+ type: String,
615
+ default: '1000px'
616
+ },
617
+ hasPX: {
618
+ type: Boolean,
619
+ default: false
620
+ },
621
+ // 导出函数
622
+ exportFn: {
623
+ type: Function,
624
+ default: null
625
+ },
626
+ operateColumnWidth: {
627
+ type: Number,
628
+ default: 180
629
+ },
630
+ // 异步获取操作按钮函数,(row) => Promise<Array<BtnItem>>
631
+ getOperateBtns: {
632
+ type: Function,
633
+ default: null
634
+ }
635
+ },
636
+ data() {
637
+ let _this = this
638
+ return {
639
+ colsKey: 0,
640
+ sizeC: localStorage.getItem('table-size') || _this.size,
641
+ sizeBind: undefined,
642
+ setTableSize: false,
643
+ key: 0,
644
+ // 悬浮按钮组相关状态
645
+ hoverRowData: null, // 当前悬停行数据
646
+ hoverRowIndex: -1, // 当前悬停行索引
647
+ showHoverBtns: false, // 是否显示悬浮按钮组
648
+ hoverBtnsPosition: { top: 0, left: 0, width: 0 }, // 按钮组位置
649
+ hoverShowTimer: null, // 显示延迟定时器
650
+ hoverHideTimer: null, // 隐藏延迟定时器
651
+ isHoverOnBtnGroup: false, // 鼠标是否在按钮组上
652
+ isDropdownVisible: false, // "更多"下拉菜单是否展开
653
+ isOperateFixed: true, // 操作按钮是否固定为列模式
654
+ hoverHeaderProp: null, // 当前悬停表头列的 prop
655
+ hoverIconKey: null, // 当前悬停的静态列头图标标识
656
+ isExpand: false,
657
+ isExport: false, // 是否导出
658
+ dialogVisible: false, // 显示列设置对话框
659
+ checkColumns: this.filterDefaultHidden(this.columns), // 显示列设置对话框中已选择的列(默认过滤 defaultHidden 列)
660
+ headerHeight: 40 // 表头实际高度,骨架屏定位用
661
+ }
662
+ },
663
+ computed: {
664
+ _columns: {
665
+ get() {
666
+ // 当操作列已通过模板固定渲染时,过滤掉 checkColumns 中的操作列,避免重复
667
+ const cols =
668
+ this.isOperateFixed && this.btnList && this.btnList.length > 0
669
+ ? this.checkColumns.filter((col) => !(col.static && col.label === this.$lc('操作')))
670
+ : this.checkColumns
671
+ if (this.isAutoWidth) {
672
+ return this.calcColumnWidth(cols)
673
+ } else {
674
+ return cols
675
+ }
676
+ },
677
+ set(val) {
678
+ return val
679
+ }
680
+ },
681
+ // 计算当前行可见的悬浮按钮
682
+ visibleHoverBtns() {
683
+ if (!this.hoverRowData || !this.btnList || this.btnList.length === 0) {
684
+ return []
685
+ }
686
+ return this.btnList.filter((btn) => this.hasBtn(btn.isHas, this.hoverRowData))
687
+ },
688
+ // 直接显示的按钮(不超过阈值)
689
+ displayHoverBtns() {
690
+ if (this.visibleHoverBtns.length > this.hoverBtnsMaxShow) {
691
+ return this.visibleHoverBtns.slice(0, this.hoverBtnsMaxShow - 1)
692
+ }
693
+ return this.visibleHoverBtns
694
+ },
695
+ // 收起到"更多"下拉菜单的按钮
696
+ moreHoverBtns() {
697
+ if (this.visibleHoverBtns.length > this.hoverBtnsMaxShow) {
698
+ return this.visibleHoverBtns.slice(this.hoverBtnsMaxShow - 1)
699
+ }
700
+ return []
701
+ },
702
+
703
+ // 悬浮按钮组样式
704
+ hoverBtnsStyle() {
705
+ return {
706
+ top: `${this.hoverBtnsPosition.top}px`,
707
+ right: 0, // 固定在右侧设置列按钮左边
708
+ height: `${this.hoverBtnsPosition.height}px`
709
+ }
710
+ },
711
+ // 计算列宽
712
+ clacStaticColumnWidth() {
713
+ const hasExpand = this.$attrs.treeConfig || this.$attrs['tree-config']
714
+ const activeCount = [this.showColumn, this.showSetsize, hasExpand].filter(Boolean).length
715
+ return activeCount === 3 ? 112 : 76
716
+ },
717
+ // 骨架屏定位样式,根据表头实际高度动态计算
718
+ skeletonStyle() {
719
+ return {
720
+ top: `${this.headerHeight}px`,
721
+ height: `calc(100% - ${this.headerHeight}px)`
722
+ }
723
+ }
724
+ },
725
+ watch: {
726
+ columns() {
727
+ this.colsKey = this.colsKey + 1
728
+ // 没有 pageId 时,columns 变更同步更新 checkColumns
729
+ if (!this.pageId) {
730
+ this.checkColumns = this.filterDefaultHidden(this.columns)
731
+ }
732
+ },
733
+ size(val) {
734
+ this.sizeC = val
735
+ },
736
+ data() {
737
+ // 翻页清除选中
738
+ if (this.clearSelect) {
739
+ this.$refs.vxeTable.clearCheckboxRow()
740
+ this.$emit('selection-change-method', [])
741
+ }
742
+ },
743
+ loading(val) {
744
+ if (val) {
745
+ this.$nextTick(() => this.updateHeaderHeight())
746
+ }
747
+ }
748
+ },
749
+ activated() {
750
+ this.$refs.vxeTable.loadData(this.data)
751
+ if (this.showColumn && this.pageId) {
752
+ this.getColumns()
753
+ }
754
+ },
755
+ beforeDestroy() {
756
+ // 清理定时器
757
+ this.clearHoverTimers()
758
+ },
759
+ mounted() {
760
+ if (this.showColumn && this.pageId) {
761
+ this.getColumns()
762
+ }
763
+ },
764
+ methods: {
765
+ handleVisibleColumn() {
766
+ this.isExport = false
767
+ this.dialogVisible = !this.dialogVisible
768
+ },
769
+ // 更新表头高度,用于骨架屏定位
770
+ updateHeaderHeight() {
771
+ const tableEl = this.$refs.vxeTable?.$el
772
+ if (!tableEl) return
773
+ const headerEl = tableEl.querySelector('.vxe-table--header-wrapper')
774
+ if (headerEl) {
775
+ this.headerHeight = headerEl.offsetHeight
776
+ }
777
+ },
778
+ openExportColumn() {
779
+ this.dialogVisible = true
780
+ this.isExport = true
781
+ },
782
+ resizableChange({ resizeWidth, column, columnIndex }) {
783
+ // 通过 prop 匹配 checkColumns 中的列,避免 _columns 过滤后索引错位
784
+ const targetCol = column?.property
785
+ ? this.checkColumns.find((c) => c.prop === column.property)
786
+ : this.checkColumns[columnIndex]
787
+ if (targetCol) {
788
+ targetCol.width = resizeWidth
789
+ }
790
+ if (this.showColumn && this.pageId) {
791
+ this.saveColumns()
792
+ }
793
+ },
794
+ // 过滤 defaultHidden 列:仅在用户未设置过显示列时作为 fallback 使用
795
+ filterDefaultHidden(columns) {
796
+ return columns.filter((col) => !col.defaultHidden)
797
+ },
798
+ // 保存列配置到后端(固定列、列宽调整等场景复用)
799
+ saveColumns() {
800
+ const userNo = sessionStorage.getItem('userNo')
801
+ // 排除模板已固定渲染的操作列,避免保存重复数据
802
+ const cols =
803
+ this.isOperateFixed && this.btnList && this.btnList.length > 0
804
+ ? this.checkColumns.filter((col) => !(col.static && col.label === this.$lc('操作')))
805
+ : this.checkColumns
806
+ const columns = saveTransform(cols, this.labelKey, this.isFilter)
807
+ axios.post(
808
+ `/bems/prod_1.0/user/pageHabit?t=${Math.random()}`,
809
+ {
810
+ userNo,
811
+ pageId: this.pageId,
812
+ showStructure: JSON.stringify(columns)
813
+ },
814
+ { loading: false }
815
+ )
816
+ },
817
+ setColumns(list) {
818
+ if (this.isExport) {
819
+ this.exportFn(list)
820
+ this.isExport = false
821
+ } else {
822
+ this.checkColumns = list
823
+ this.colsKey++
824
+ }
825
+ },
826
+ async getColumns() {
827
+ try {
828
+ const columns = await this.$refs?.showColumn?.getColumns(this.pageId)
829
+
830
+ if (columns?.length) {
831
+ this.checkColumns = columns
832
+ // remote: true 的远程列同步写入 this.columns,保持位置一致
833
+ this.syncRemoteColumns(columns)
834
+ // 强制 vxe-table 重建,确保固定列图标、颜色等表头状态正确回显
835
+ this.colsKey++
836
+ } else {
837
+ this.checkColumns = this.filterDefaultHidden(this.columns)
838
+ }
839
+ } catch (err) {
840
+ // API 请求失败时回退到过滤后的 columns prop
841
+ this.checkColumns = this.filterDefaultHidden(this.columns)
842
+ }
843
+ },
844
+ // 供组件外部调用,刷新表头数据(重新请求 getColumns 并同步远程列)
845
+ async refreshColumns() {
846
+ if (this.showColumn && this.pageId) {
847
+ await this.getColumns()
848
+ }
849
+ },
850
+ // API 返回中标记 remote: true 的列同步到 this.columns,保持对应位置一致
851
+ syncRemoteColumns(apiColumns) {
852
+ apiColumns.forEach((apiCol, index) => {
853
+ if (!apiCol.remote) return
854
+ // prop 或 labelKey 匹配 this.columns 中是否已存在
855
+ const matchKey = apiCol.prop ? 'prop' : this.labelKey
856
+ const existingIdx = this.columns.findIndex((c) => c[matchKey] === apiCol[matchKey])
857
+ if (existingIdx !== -1) {
858
+ // 已存在则替换,保持位置以 API 返回为准
859
+ this.$set(this.columns, existingIdx, apiCol)
860
+ } else {
861
+ // 不存在则插入到对应位置(避免 $set 索引超出长度产生稀疏数组)
862
+ const insertIdx = Math.min(index, this.columns.length)
863
+ this.columns.splice(insertIdx, 0, apiCol)
864
+ }
865
+ })
866
+ },
867
+ /**
868
+ * 一键展开/折叠表格树形行(仅处理第一层级数据行)
869
+ * 通过 vxe-table 的 setTreeExpand API 批量设置顶层行的展开状态,
870
+ * await Promise 完成后再同步 isExpand 状态,保证图标与实际展开状态一致
871
+ * 不直接操作 DOM,不影响排序、筛选等原有逻辑
872
+ * @returns {Promise<void>}
873
+ */
874
+ async toggleExpand() {
875
+ const $table = this.$refs.vxeTable
876
+ if (!$table) return
877
+ // 仅取顶层数据行(data prop 中的第一层,不包含子级)
878
+ const topLevelRows = this.data || []
879
+ if (topLevelRows.length === 0) return
880
+ // 切换展开状态:当前已展开则折叠,当前已折叠则展开
881
+ const nextExpand = !this.isExpand
882
+ // setTreeExpand 返回 Promise,await 确保展开动作完成后再更新图标状态
883
+ await $table.setTreeExpand(topLevelRows, nextExpand)
884
+ this.isExpand = nextExpand
885
+ },
886
+ /**
887
+ * 同步单行手动展开/折叠后的图标状态
888
+ * 监听 vxe-table toggle-tree-expand 事件,根据当前所有顶层行展开情况更新 isExpand
889
+ * @param {Object} expandRecords - vxe-table 当前所有已展开的行记录
890
+ * @returns {void}
891
+ */
892
+ syncExpandState({ expandRecords }) {
893
+ const topLevelRows = this.data || []
894
+ if (topLevelRows.length === 0) return
895
+ // 若顶层行全部处于展开状态则认为是"已展开",否则为"未全部展开"
896
+ const expandSet = new Set(expandRecords)
897
+ this.isExpand = topLevelRows.every((row) => expandSet.has(row))
898
+ },
899
+ // 锁定列:将该列 fixed 设为 'left',直接修改 prop 对象并刷新表格
900
+ lockColumn(item) {
901
+ if (item.fixed || item.static) return
902
+ this.$set(item, 'fixed', 'left')
903
+ // 手动触发 colsKey 刷新,让 vxe-table 重新渲染列配置
904
+ this.colsKey = this.colsKey + 1
905
+ // 固定列后自动保存到后端
906
+ if (this.showColumn && this.pageId) {
907
+ this.saveColumns()
908
+ }
909
+ },
910
+ // 解锁列:移除 fixed 属性,将该列恢复为普通列
911
+ unlockColumn(item) {
912
+ if (item.fixed !== 'left') return
913
+ this.$delete(item, 'fixed')
914
+ // 手动触发 colsKey 刷新,让 vxe-table 重新渲染列配置
915
+ this.colsKey = this.colsKey + 1
916
+ // 取消固定后自动保存到后端
917
+ if (this.showColumn && this.pageId) {
918
+ this.saveColumns()
919
+ }
920
+ },
921
+ // 固定操作列为内联列
922
+ fixOperateColumn() {
923
+ this.isOperateFixed = true
924
+ this.hideHoverBtns()
925
+ this.colsKey = this.colsKey + 1
926
+ this.$emit('operate-fixed-change', true)
927
+ },
928
+ // 取消固定,切换回悬浮按钮组
929
+ unfixOperateColumn() {
930
+ this.isOperateFixed = false
931
+ this.colsKey = this.colsKey + 1
932
+ this.$emit('operate-fixed-change', false)
933
+ },
934
+ // 处理固定操作列的按钮事件
935
+ handleOperateCommand(command, row, rowIndex) {
936
+ this.$emit(command, row, rowIndex)
937
+ //
938
+ const btn = this.btnList.find((item) => item.command === command || item.label === command) || { command }
939
+ this.$emit('hover-btn-click', { btn, row, rowIndex })
940
+ },
941
+ // 处理 tableOperate 固定/取消固定切换
942
+ handleToggleOperateFixed(fixed) {
943
+ if (fixed) {
944
+ this.fixOperateColumn()
945
+ } else {
946
+ this.unfixOperateColumn()
947
+ }
948
+ },
949
+ // 判断按鈕是否显示
950
+ hasBtn(isHas, row) {
951
+ if (isHas === undefined || isHas === null) {
952
+ return true
953
+ } else if (typeof isHas === 'boolean') {
954
+ return isHas
955
+ } else if (typeof isHas === 'string' || Array.isArray(isHas)) {
956
+ return this.$has(isHas)
957
+ } else if (typeof isHas === 'function') {
958
+ return isHas(row)
959
+ }
960
+ },
961
+ // 解析 badge 值,支持 number、string、function
962
+ resolveBadge(btn) {
963
+ if (!btn.badge) return null
964
+ if (typeof btn.badge === 'function') {
965
+ const val = btn.badge(this.hoverRowData)
966
+ return val || val === 0 ? val : null
967
+ }
968
+ return btn.badge
969
+ },
970
+ // 解析 disabled 值,支持 boolean 和 function
971
+ resolveDisabled(btn) {
972
+ if (btn.disabled === undefined || btn.disabled === null) {
973
+ return false
974
+ } else if (typeof btn.disabled === 'boolean') {
975
+ return btn.disabled
976
+ } else if (typeof btn.disabled === 'function') {
977
+ return btn.disabled(this.hoverRowData)
978
+ }
979
+ return false
980
+ },
981
+ // 全选/反选
982
+ toggleAllSelection() {
983
+ if (!this.$refs.vxeTable) return
984
+ const allData = this.data || []
985
+ // 获取合并行索引集合
986
+ const mergedIndexes =
987
+ this.excludeMergedRows && this.mergeCells && this.mergeCells.length > 0 ? this.getMergedRowIndexes() : new Set()
988
+ // 筛选出可选择的行(排除合并从属行 + 排除禁止选择的行)
989
+ const selectableRows = allData.filter((row, index) => {
990
+ if (mergedIndexes.has(index)) return false
991
+ if (typeof this.forbidSelect === 'function' && this.forbidSelect({ row }) === true) return false
992
+ return true
993
+ })
994
+ // 判断当前可选行是否已全部选中
995
+ const checkedRows = this.$refs.vxeTable.getCheckboxRecords(false)
996
+ const checkedSet = new Set(checkedRows)
997
+ const isAllChecked = selectableRows.length > 0 && selectableRows.every((row) => checkedSet.has(row))
998
+ if (isAllChecked) {
999
+ // 已全选,则取消可选行的选中状态
1000
+ selectableRows.forEach((row) => {
1001
+ this.$refs.vxeTable.setCheckboxRow(row, false)
1002
+ })
1003
+ } else {
1004
+ // 未全选,则选中所有可选行
1005
+ selectableRows.forEach((row) => {
1006
+ this.$refs.vxeTable.setCheckboxRow(row, true)
1007
+ })
1008
+ }
1009
+ // 得手动触发
1010
+ this.handleSelectionChange()
1011
+ },
1012
+ // 获取被合并的从属行索引集合
1013
+ getMergedRowIndexes() {
1014
+ const mergedIndexes = new Set()
1015
+ if (this.mergeCells && this.mergeCells.length > 0) {
1016
+ this.mergeCells.forEach((cell) => {
1017
+ if (cell.rowspan > 1) {
1018
+ // 从 row+1 到 row+rowspan-1 都是被合并的从属行
1019
+ for (let i = 1; i < cell.rowspan; i++) {
1020
+ mergedIndexes.add(cell.row + i)
1021
+ }
1022
+ }
1023
+ })
1024
+ }
1025
+ return mergedIndexes
1026
+ },
1027
+
1028
+ // 清空选择
1029
+ clearSelection() {
1030
+ if (this.$refs.vxeTable) {
1031
+ this.$refs.vxeTable.setAllCheckboxRow(false)
1032
+ // 得手动触发
1033
+ this.handleSelectionChange()
1034
+ }
1035
+ },
1036
+ // 选中某些行
1037
+ toggleRowSelection(row, state = true) {
1038
+ this.$refs.vxeTable.setCheckboxRow(row, state)
1039
+ // 得手动触发
1040
+ this.handleSelectionChange()
1041
+ },
1042
+ toggleAll($table, disabled, event) {
1043
+ // 阻止事件冒泡,防止触发其他点击事件
1044
+ if (event) {
1045
+ event.stopPropagation()
1046
+ }
1047
+ if (disabled) {
1048
+ return
1049
+ }
1050
+ // 如果配置了排除合并行,则手动处理非合并行的选中状态
1051
+ if (this.excludeMergedRows && this.mergeCells && this.mergeCells.length > 0) {
1052
+ const mergedIndexes = this.getMergedRowIndexes()
1053
+ const allData = this.data || []
1054
+ // 筛选出可选择的行(排除合并从属行 + 排除禁止选择的行)
1055
+ const selectableRows = allData.filter((row, index) => {
1056
+ if (mergedIndexes.has(index)) return false
1057
+ if (typeof this.forbidSelect === 'function' && this.forbidSelect({ row }) === true) return false
1058
+ return true
1059
+ })
1060
+ // 判断当前可选行是否已全部选中
1061
+ const checkedRows = $table.getCheckboxRecords(false)
1062
+ const checkedSet = new Set(checkedRows)
1063
+ const isAllChecked = selectableRows.length > 0 && selectableRows.every((row) => checkedSet.has(row))
1064
+ if (isAllChecked) {
1065
+ // 已全选,则取消可选行的选中状态
1066
+ selectableRows.forEach((row) => {
1067
+ $table.setCheckboxRow(row, false)
1068
+ })
1069
+ } else {
1070
+ // 未全选,则选中所有可选行
1071
+ selectableRows.forEach((row) => {
1072
+ $table.setCheckboxRow(row, true)
1073
+ })
1074
+ }
1075
+ } else {
1076
+ // 默认行为:全选/取消全选所有行
1077
+ $table.toggleAllCheckboxRow()
1078
+ }
1079
+ this.key++
1080
+ this.handleSelectionChange()
1081
+ },
1082
+ toggleChecked($table, row, disabled) {
1083
+ if (disabled) {
1084
+ return false
1085
+ }
1086
+ $table.toggleCheckboxRow(row)
1087
+ this.key++
1088
+ this.handleSelectionChange(row)
1089
+ },
1090
+ customSortMethod({ sortList }) {
1091
+ const orders = sortList.map((item) => {
1092
+ return {
1093
+ column: item.field,
1094
+ asc: item.order === 'asc'
1095
+ }
1096
+ })
1097
+ this.$emit('sort-change-method', orders)
1098
+ },
1099
+ filterChange(data) {
1100
+ const { filterList } = data
1101
+ // 输出全部条件
1102
+ const obj = {}
1103
+ // 复制默认筛选条件为空
1104
+ this.checkColumns.forEach((item) => {
1105
+ if (item.filters) {
1106
+ this.$set(obj, item.prop, undefined)
1107
+ }
1108
+ })
1109
+ filterList.forEach((item) => {
1110
+ if (item.column.filterMultiple) {
1111
+ this.$set(obj, item.field, item.values)
1112
+ } else {
1113
+ this.$set(obj, item.field, item.values && item.values[0])
1114
+ }
1115
+ })
1116
+ // 输出单个条件
1117
+ const singleObj = {}
1118
+ for (let key in obj) {
1119
+ if (key === data.field) {
1120
+ singleObj[data.field] = obj[data.field]
1121
+ }
1122
+ }
1123
+ if (this.isFiliterSingle) {
1124
+ this.$emit('filter-change-method', singleObj)
1125
+ } else {
1126
+ this.$emit('filter-change-method', obj)
1127
+ }
1128
+ },
1129
+ // row当前单次勾选的哪一行数据 包含checked字段
1130
+ handleSelectionChange(row = '') {
1131
+ const val = this.$refs.vxeTable.getCheckboxRecords(false)
1132
+ // 支持跨页勾选
1133
+ const val1 = this.$refs.vxeTable.getCheckboxReserveRecords()
1134
+
1135
+ this.$emit('selection-change-method', [...val, ...val1], row)
1136
+ },
1137
+ // 点击行勾选/取消勾选
1138
+ handleCellClick({ row, column, $event }) {
1139
+ this.$emit('cell-click', { row, column, $event })
1140
+ // 如果点击的是 checkbox 列,不处理(由原生的 checkbox 处理)
1141
+ if (column.type === 'checkbox') {
1142
+ return
1143
+ }
1144
+
1145
+ // 检查点击的目标元素是否是按钮或链接
1146
+ // 需要检测多种按钮类型:
1147
+ // - button: 原生按钮标签
1148
+ // - .el-button: Element UI 按钮组件
1149
+ // - .n20-button: n20 项目按钮组件
1150
+ // - [class*="button"]: 包含 button 类名的元素
1151
+ // - [class*="el-link--inner"]: Element UI 链接组件内部元素
1152
+ // - a[href]: 链接标签
1153
+ const target = $event.target
1154
+ const clickedButton = target.closest(
1155
+ 'button, .el-button, .n20-button, [class*="button"], [class*="el-link--inner"], a[href]'
1156
+ )
1157
+
1158
+ // 如果点击的是按钮或链接,不触发行勾选
1159
+ if (clickedButton) {
1160
+ return
1161
+ }
1162
+
1163
+ // 如果 closest 没找到,检查父元素的类名是否包含操作相关的关键词
1164
+ if (!clickedButton && target.parentElement) {
1165
+ const parentClass = target.parentElement.className || ''
1166
+ // 检测父元素是否包含:operate(操作)、btn(按钮)、button(按钮) 等关键词
1167
+ if (parentClass.includes('operate') || parentClass.includes('btn') || parentClass.includes('button')) {
1168
+ return
1169
+ }
1170
+ }
1171
+
1172
+ // 检查该行是否可以被勾选
1173
+ let canCheck = true
1174
+ if (typeof this.forbidSelect === 'function') {
1175
+ const forbidResult = this.forbidSelect({ row })
1176
+ // 只有当 forbidSelect 明确返回 true 时,才允许勾选
1177
+ canCheck = forbidResult !== false
1178
+ }
1179
+
1180
+ if (canCheck) {
1181
+ // 切换该行的勾选状态
1182
+ this.$refs.vxeTable.toggleCheckboxRow(row)
1183
+ this.key++ // 强制刷新 checkbox 显示
1184
+ this.handleSelectionChange(row)
1185
+ }
1186
+ },
1187
+ sizeUp(size) {
1188
+ this.sizeC = size
1189
+ this.$emit('update:size', size)
1190
+ },
1191
+ sizeSet(el) {
1192
+ this.sizeBind = el
1193
+ },
1194
+ // 计算列宽
1195
+ // ========== 悬浮按钮组相关方法 ==========
1196
+ // 清理所有定时器
1197
+ clearHoverTimers() {
1198
+ if (this.hoverShowTimer) {
1199
+ clearTimeout(this.hoverShowTimer)
1200
+ this.hoverShowTimer = null
1201
+ }
1202
+ if (this.hoverHideTimer) {
1203
+ clearTimeout(this.hoverHideTimer)
1204
+ this.hoverHideTimer = null
1205
+ }
1206
+ },
1207
+ // 鼠标进入单元格事件(防抖处理)
1208
+ handleCellMouseEnter({ row, rowIndex, $rowIndex, $event }) {
1209
+ // 如果没有配置悬浮按钮,或已固定为列模式,不处理
1210
+ if (!this.btnList || this.btnList.length === 0 || this.isOperateFixed) {
1211
+ return
1212
+ }
1213
+ // 清理隐藏定时器
1214
+ if (this.hoverHideTimer) {
1215
+ clearTimeout(this.hoverHideTimer)
1216
+ this.hoverHideTimer = null
1217
+ }
1218
+ // 如果是同一行,不重复处理
1219
+ if (this.hoverRowIndex === rowIndex && this.showHoverBtns) {
1220
+ return
1221
+ }
1222
+ // 清理之前的显示定时器
1223
+ if (this.hoverShowTimer) {
1224
+ clearTimeout(this.hoverShowTimer)
1225
+ }
1226
+ // 延迟显示(防抖)
1227
+ this.hoverShowTimer = setTimeout(() => {
1228
+ this.updateHoverRow(row, rowIndex, $event)
1229
+ }, this.hoverBtnsDelay)
1230
+ },
1231
+ // 鼠标离开单元格事件(防抖处理)
1232
+ handleCellMouseLeave({ row, rowIndex, $event }) {
1233
+ // 如果没有配置悬浮按钮,或已固定为列模式,不处理
1234
+ if (!this.btnList || this.btnList.length === 0 || this.isOperateFixed) {
1235
+ return
1236
+ }
1237
+ // 清理显示定时器
1238
+ if (this.hoverShowTimer) {
1239
+ clearTimeout(this.hoverShowTimer)
1240
+ this.hoverShowTimer = null
1241
+ }
1242
+ // 延迟隐藏(给鼠标移动到按钮组的时间)
1243
+ this.hoverHideTimer = setTimeout(() => {
1244
+ if (!this.isHoverOnBtnGroup) {
1245
+ this.hideHoverBtns()
1246
+ }
1247
+ }, this.hoverBtnsHideDelay)
1248
+ },
1249
+ // 更新悬停行信息并显示按钮组
1250
+ updateHoverRow(row, rowIndex, $event) {
1251
+ this.hoverRowData = row
1252
+ this.hoverRowIndex = rowIndex
1253
+ // 计算按钮组位置
1254
+ this.calculateBtnPosition($event)
1255
+ // 显示按钮组
1256
+ this.showHoverBtns = true
1257
+ // 触发事件
1258
+ this.$emit('row-hover-enter', { row, rowIndex })
1259
+ },
1260
+ // 计算按钮组位置
1261
+ calculateBtnPosition($event) {
1262
+ const tableEl = this.$refs.vxeTable?.$el
1263
+ if (!tableEl) return
1264
+ // 获取当前行的 DOM 元素
1265
+ const rowEl = $event?.target?.closest('tr.vxe-body--row')
1266
+ if (!rowEl) return
1267
+ // 获取表格容器的位置信息
1268
+ const tableRect = tableEl.getBoundingClientRect()
1269
+ const rowRect = rowEl.getBoundingClientRect()
1270
+ // 计算相对于表格容器的位置
1271
+ this.hoverBtnsPosition = {
1272
+ top: rowRect.top - tableRect.top,
1273
+ height: rowRect.height
1274
+ }
1275
+ },
1276
+ // 隐藏悬浮按钮组
1277
+ hideHoverBtns() {
1278
+ // 如果下拉菜单正在展开,不隐藏
1279
+ if (this.isDropdownVisible) {
1280
+ return
1281
+ }
1282
+ // 移除当前行的 hover 类名
1283
+ this.removeRowHoverClass()
1284
+ this.showHoverBtns = false
1285
+ this.hoverRowData = null
1286
+ this.hoverRowIndex = -1
1287
+ this.isHoverOnBtnGroup = false
1288
+ // 触发事件
1289
+ this.$emit('row-hover-leave')
1290
+ },
1291
+ // 鼠标进入按钮组
1292
+ handleBtnGroupEnter() {
1293
+ this.isHoverOnBtnGroup = true
1294
+ // 清理隐藏定时器
1295
+ if (this.hoverHideTimer) {
1296
+ clearTimeout(this.hoverHideTimer)
1297
+ this.hoverHideTimer = null
1298
+ }
1299
+ // 给当前行添加 hover 类名
1300
+ this.addRowHoverClass()
1301
+ },
1302
+ // 鼠标离开按钮组
1303
+ handleBtnGroupLeave() {
1304
+ this.isHoverOnBtnGroup = false
1305
+ // 如果下拉菜单正在展开,不隐藏按钮组
1306
+ if (this.isDropdownVisible) {
1307
+ return
1308
+ }
1309
+ // 移除当前行的 hover 类名
1310
+ this.removeRowHoverClass()
1311
+ // 延迟隐藏
1312
+ this.hoverHideTimer = setTimeout(() => {
1313
+ // 再次检查下拉菜单状态
1314
+ if (!this.isDropdownVisible) {
1315
+ this.hideHoverBtns()
1316
+ }
1317
+ }, this.hoverBtnsHideDelay)
1318
+ },
1319
+ // 处理下拉菜单显示/隐藏状态变化
1320
+ handleDropdownVisibleChange(visible) {
1321
+ this.isDropdownVisible = visible
1322
+ if (!visible) {
1323
+ // 下拉菜单关闭后,延迟检查是否需要隐藏按钮组
1324
+ this.hoverHideTimer = setTimeout(() => {
1325
+ if (!this.isHoverOnBtnGroup && !this.isDropdownVisible) {
1326
+ this.hideHoverBtns()
1327
+ }
1328
+ }, this.hoverBtnsHideDelay)
1329
+ }
1330
+ },
1331
+ // 给当前悬停行添加 hover 类名
1332
+ addRowHoverClass() {
1333
+ const tableEl = this.$refs.vxeTable?.$el
1334
+ if (!tableEl || this.hoverRowIndex < 0) return
1335
+ // 查找当前行的 tr 元素
1336
+ const rows = tableEl.querySelectorAll('tr.vxe-body--row')
1337
+ rows.forEach((row, index) => {
1338
+ if (index === this.hoverRowIndex) {
1339
+ row.classList.add('row--hover')
1340
+ }
1341
+ })
1342
+ },
1343
+ // 移除当前悬停行的 hover 类名
1344
+ removeRowHoverClass() {
1345
+ const tableEl = this.$refs.vxeTable?.$el
1346
+ if (!tableEl) return
1347
+ // 移除所有行的 hover 类名
1348
+ const rows = tableEl.querySelectorAll('tr.vxe-body--row.row--hover')
1349
+ rows.forEach((row) => {
1350
+ row.classList.remove('row--hover')
1351
+ })
1352
+ },
1353
+ // 处理悬浮按钮点击
1354
+ handleHoverBtnClick(btn, $event) {
1355
+ if (this.resolveDisabled(btn)) return
1356
+
1357
+ if (typeof btn.command === 'function') {
1358
+ btn.command(this.hoverRowData, this.hoverRowIndex, $event)
1359
+ } else if (typeof btn.command === 'string') {
1360
+ this.$emit(btn.command, this.hoverRowData, this.hoverRowIndex, $event)
1361
+ }
1362
+ // 触发事件
1363
+ this.$emit('hover-btn-click', {
1364
+ btn,
1365
+ row: this.hoverRowData,
1366
+ rowIndex: this.hoverRowIndex,
1367
+ $event
1368
+ })
1369
+ },
1370
+ // 处理"更多"下拉菜单命令
1371
+ handleMoreBtnCommand(btn) {
1372
+ if (!btn || this.resolveDisabled(btn)) return
1373
+ this.handleHoverBtnClick(btn, null)
1374
+ },
1375
+ // 表格滚动时隐藏按钮组
1376
+ handleTableScroll() {
1377
+ if (this.showHoverBtns) {
1378
+ this.hideHoverBtns()
1379
+ }
1380
+ },
1381
+ // ========== 列宽计算相关方法 ==========
1382
+ getRowClassName(args) {
1383
+ const rowClassName = this.$attrs['row-class-name'] || this.$attrs.rowClassName
1384
+ // 用户传入了 row-class-name,以传入的为准
1385
+ if (rowClassName && typeof rowClassName === 'function') {
1386
+ return rowClassName(args)
1387
+ }
1388
+ if (rowClassName && typeof rowClassName === 'string') {
1389
+ return rowClassName
1390
+ }
1391
+ // 用户未传入,选中行高亮
1392
+ const checkedRows = this.$refs.vxeTable?.getCheckboxRecords(false) || []
1393
+ const checkedSet = new Set(checkedRows)
1394
+ return checkedSet.has(args.row) ? 'v3-n20-table-pro__row-checked' : ''
1395
+ },
1396
+
1397
+ calcColumnWidth(columns) {
1398
+ // 常量配置
1399
+ const CHAR_WIDTH = 20 // 每个字符的平均宽度(像素)
1400
+ const PADDING = 20 // 左右padding
1401
+ const CHECKBOX_WIDTH = 50 // 复选框列宽度
1402
+ const OPERATE_WIDTH = 180 // 操作列宽度
1403
+ const HOVER_BTNS_WIDTH = 356 // 悬浮按钮组宽度
1404
+ const MIN_LABEL_LENGTH = 10 // 最小标签长度
1405
+
1406
+ // 解析宽度值(支持数字、"100"、"100px"格式)
1407
+ const parseWidth = (value) => (typeof value === 'number' ? value : parseInt(value, 10) || 0)
1408
+
1409
+ const calcColumns = columns.map((column) => {
1410
+ // 操作列固定宽度
1411
+ if (column.static && column.label === this.$lc('操作') && !column.width && !column.minWidth) {
1412
+ column.width = OPERATE_WIDTH
1413
+ }
1414
+ // 复选框列固定宽度
1415
+ if (column.type === 'checkbox') {
1416
+ column.width = CHECKBOX_WIDTH
1417
+ }
1418
+ // 未设置宽度时,根据标签字符数计算最小宽度
1419
+ if (!column.width && !column.minWidth && !column['min-width']) {
1420
+ const textLength = column.label?.length || MIN_LABEL_LENGTH
1421
+ column.minWidth = Math.ceil(textLength * CHAR_WIDTH + PADDING)
1422
+ }
1423
+ // 计算基础宽度
1424
+ const widthValue = column.width || column.minWidth || column['min-width'] || 0
1425
+ const baseWidth = parseWidth(widthValue)
1426
+ column._baseWidth_ = baseWidth
1427
+ return column
1428
+ })
1429
+
1430
+ // 悬浮按钮:给最后一列预留按钮组空间(固定模式下不需要)
1431
+ if (!this.isOperateFixed && this.btnList?.length && calcColumns.length > 0) {
1432
+ const lastColumn = calcColumns[calcColumns.length - 1]
1433
+ const baseWidth = lastColumn._baseWidth_ || 0
1434
+ if (baseWidth <= HOVER_BTNS_WIDTH) {
1435
+ lastColumn.width = HOVER_BTNS_WIDTH + baseWidth
1436
+ }
1437
+ lastColumn.align = 'left'
1438
+ }
1439
+ return calcColumns
1440
+ }
1441
+ }
1442
+ }
1443
+ </script>