xt-element-ui 1.2.2 → 1.2.3

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.
@@ -8,113 +8,16 @@
8
8
  </div>
9
9
  </div>
10
10
 
11
- <!-- 虚拟滚动固定表头:复用 el-table 列布局,与 body 表头对齐 -->
12
- <div
13
- v-if="virtualScroll && computedHeight"
14
- ref="fixedHeader"
15
- class="ex-table-fixed-header"
16
- >
17
- <el-table
18
- ref="headerTable"
19
- :data="headerTableData"
20
- class="ex-table ex-table-header-only"
21
- >
22
- <el-table-column v-if="selection" type="selection" width="55" :fixed="selectionFixed" />
23
- <el-table-column
24
- v-if="showIndex"
25
- type="index"
26
- width="60"
27
- label="#"
28
- :fixed="indexFixed"
29
- />
30
- <template v-for="col in flattenedColumns">
31
- <el-table-column
32
- v-if="col.children && col.children.length"
33
- :key="col._key"
34
- v-bind="getColumnProps(col)"
35
- >
36
- <el-table-column
37
- v-for="child in col.children"
38
- :key="child._key || child.prop"
39
- v-bind="getColumnProps(child)"
40
- />
41
- </el-table-column>
42
- <el-table-column v-else :key="col._key" v-bind="getColumnProps(col)" />
43
- </template>
44
- </el-table>
45
- </div>
46
-
47
- <!-- 表格主体 -->
48
- <div
49
- class="ex-table-body"
50
- :class="{ 'ex-table-virtual': virtualScroll }"
51
- :style="virtualScroll ? { height: computedHeight + 'px' } : {}"
52
- ref="tableBody"
53
- @scroll="handleBodyScroll"
54
- >
55
- <!-- 虚拟滚动:包裹层 + 占位撑开 -->
56
- <div v-if="virtualScroll" class="ex-table-virtual-inner" :style="{ height: virtualTotalHeight + 'px' }">
57
- <div :style="{ transform: 'translateY(' + virtualOffsetY + 'px)' }">
58
- <el-table
59
- ref="table"
60
- :data="visibleData"
61
- :span-method="groupColumns.length ? handleSpanMethod : undefined"
62
- :row-class-name="getRowClassName"
63
- :show-header="false"
64
- v-bind="$attrs"
65
- v-on="$listeners"
66
- @selection-change="handleSelectionChange"
67
- @sort-change="handleSortChange"
68
- class="ex-table"
69
- >
70
- <el-table-column v-if="selection" type="selection" width="55" :fixed="selectionFixed" />
71
- <el-table-column
72
- v-if="showIndex"
73
- type="index"
74
- width="60"
75
- label="#"
76
- :fixed="indexFixed"
77
- :index="indexMethod"
78
- />
79
- <template v-for="col in flattenedColumns">
80
- <el-table-column
81
- v-if="col.children && col.children.length"
82
- :key="col._key"
83
- v-bind="getColumnProps(col)"
84
- >
85
- <template v-for="child in col.children">
86
- <el-table-column :key="child._key" v-bind="getColumnProps(child)">
87
- <template v-if="child.render" v-slot="scope">{{ child.render(scope) }}</template>
88
- <template v-else-if="child.formatter" v-slot="scope">
89
- <ExTableCell :row="scope.row" :index="scope.$index" :formatter="child.formatter" :column="child" />
90
- </template>
91
- <template v-else-if="child.slot" v-slot="scope">
92
- <slot :name="child.slot" :row="scope.row" :index="scope.$index" :column="child" />
93
- </template>
94
- </el-table-column>
95
- </template>
96
- </el-table-column>
97
- <el-table-column v-else :key="col._key" v-bind="getColumnProps(col)">
98
- <template v-if="col.render" v-slot="scope">{{ col.render(scope) }}</template>
99
- <template v-else-if="col.formatter" v-slot="scope">
100
- <ExTableCell :row="scope.row" :index="scope.$index" :formatter="col.formatter" :column="col" />
101
- </template>
102
- <template v-else-if="col.slot" v-slot="scope">
103
- <slot :name="col.slot" :row="scope.row" :index="scope.$index" :column="col" />
104
- </template>
105
- </el-table-column>
106
- </template>
107
- </el-table>
108
- </div>
109
- </div>
110
-
111
- <!-- 非虚拟滚动:直接渲染 -->
112
- <el-table
113
- v-else
11
+ <!-- 主体表格:单 Table + 内置虚拟滚动(核心改造) -->
12
+ <div class="ex-table-body">
13
+ <VirtualElTable
114
14
  ref="table"
115
- :data="visibleData"
15
+ :data="processedTableData"
116
16
  :height="computedHeight"
117
17
  :max-height="computedMaxHeight"
18
+ :virtual-scroll="virtualScroll"
19
+ :row-height="rowInitHeight"
20
+ :buffer-size="bufferSize"
118
21
  :span-method="groupColumns.length ? handleSpanMethod : undefined"
119
22
  :row-class-name="getRowClassName"
120
23
  v-bind="$attrs"
@@ -123,7 +26,14 @@
123
26
  @sort-change="handleSortChange"
124
27
  class="ex-table"
125
28
  >
126
- <el-table-column v-if="selection" type="selection" width="55" :fixed="selectionFixed" />
29
+ <!-- 选择列 -->
30
+ <el-table-column
31
+ v-if="selection"
32
+ type="selection"
33
+ width="55"
34
+ :fixed="selectionFixed"
35
+ />
36
+ <!-- 序号列 -->
127
37
  <el-table-column
128
38
  v-if="showIndex"
129
39
  type="index"
@@ -132,6 +42,7 @@
132
42
  :fixed="indexFixed"
133
43
  :index="indexMethod"
134
44
  />
45
+ <!-- 多级列配置 -->
135
46
  <template v-for="col in flattenedColumns">
136
47
  <el-table-column
137
48
  v-if="col.children && col.children.length"
@@ -142,10 +53,20 @@
142
53
  <el-table-column :key="child._key" v-bind="getColumnProps(child)">
143
54
  <template v-if="child.render" v-slot="scope">{{ child.render(scope) }}</template>
144
55
  <template v-else-if="child.formatter" v-slot="scope">
145
- <ExTableCell :row="scope.row" :index="scope.$index" :formatter="child.formatter" :column="child" />
56
+ <ExTableCell
57
+ :row="scope.row"
58
+ :index="scope.$index"
59
+ :formatter="child.formatter"
60
+ :column="child"
61
+ />
146
62
  </template>
147
63
  <template v-else-if="child.slot" v-slot="scope">
148
- <slot :name="child.slot" :row="scope.row" :index="scope.$index" :column="child" />
64
+ <slot
65
+ :name="child.slot"
66
+ :row="scope.row"
67
+ :index="scope.$index"
68
+ :column="child"
69
+ />
149
70
  </template>
150
71
  </el-table-column>
151
72
  </template>
@@ -153,14 +74,24 @@
153
74
  <el-table-column v-else :key="col._key" v-bind="getColumnProps(col)">
154
75
  <template v-if="col.render" v-slot="scope">{{ col.render(scope) }}</template>
155
76
  <template v-else-if="col.formatter" v-slot="scope">
156
- <ExTableCell :row="scope.row" :index="scope.$index" :formatter="col.formatter" :column="col" />
77
+ <ExTableCell
78
+ :row="scope.row"
79
+ :index="scope.$index"
80
+ :formatter="col.formatter"
81
+ :column="col"
82
+ />
157
83
  </template>
158
84
  <template v-else-if="col.slot" v-slot="scope">
159
- <slot :name="col.slot" :row="scope.row" :index="scope.$index" :column="col" />
85
+ <slot
86
+ :name="col.slot"
87
+ :row="scope.row"
88
+ :index="scope.$index"
89
+ :column="col"
90
+ />
160
91
  </template>
161
92
  </el-table-column>
162
93
  </template>
163
- </el-table>
94
+ </VirtualElTable>
164
95
  </div>
165
96
 
166
97
  <!-- 分页 -->
@@ -180,68 +111,39 @@
180
111
 
181
112
  <script>
182
113
  import ExTableCell from './ExTableCell.vue'
183
- // 内部导入 element-ui 组件,避免与外部 element-ui 冲突
184
- import { Table as ElTable, TableColumn as ElTableColumn } from 'element-ui'
114
+ import VirtualElTable from './VirtualElTable.vue'
185
115
 
186
116
  export default {
187
117
  name: 'ExTable',
188
118
  inheritAttrs: false,
189
- components: { ExTableCell, ElTable, ElTableColumn },
119
+ components: { ExTableCell, VirtualElTable },
120
+
190
121
  props: {
191
- // 表格数据
192
122
  tableData: { type: Array, default: () => [] },
193
- // 列配置(支持 children 多级表头)
194
123
  columns: { type: Array, default: () => [] },
195
- // 分组列配置(按配置的层级合并单元格)
196
124
  groupColumns: { type: Array, default: () => [] },
197
- // 标题
198
125
  title: { type: String, default: '' },
199
- // 高度
200
126
  height: { type: [Number, String], default: null },
201
- // 最大高度(非虚拟滚动时与 height 二选一,均支持固定表头)
202
127
  maxHeight: { type: [Number, String], default: null },
203
- // 虚拟滚动
204
128
  virtualScroll: { type: Boolean, default: false },
205
- // 预估行高
206
129
  rowInitHeight: { type: Number, default: 48 },
207
- // 缓冲区行数
208
130
  bufferSize: { type: Number, default: 5 },
209
- // 分页配置
210
131
  pagination: { type: Object, default: null },
211
- // 总条数
212
132
  total: { type: Number, default: 0 },
213
- // 是否显示序号
214
133
  showIndex: { type: Boolean, default: false },
215
- // 是否可选
216
134
  selection: { type: Boolean, default: false },
217
- // 选择列固定位置
218
135
  selectionFixed: { type: [String, Boolean], default: false },
219
- // 序号列固定位置
220
136
  indexFixed: { type: [String, Boolean], default: false },
221
- // 加载状态
222
137
  loading: { type: Boolean, default: false },
223
- // 空数据文案
224
138
  emptyText: { type: String, default: '暂无数据' },
225
- // 小计配置
226
139
  subtotalConfig: { type: Object, default: () => ({ enabled: false }) },
227
- // 总计配置
228
140
  totalConfig: { type: Object, default: () => ({ enabled: false }) }
229
141
  },
230
142
 
231
143
  data() {
232
144
  return {
233
- // 虚拟滚动相关
234
- scrollTop: 0,
235
- scrollLeft: 0,
236
- rowHeight: 48,
237
- visibleStartIndex: 0,
238
- visibleEndIndex: 0,
239
- visibleCount: 0,
240
- // 分组合并缓存
241
145
  spanCache: {},
242
- // 列扁平化缓存
243
146
  flattenedColumnsCache: [],
244
- // 选中数据
245
147
  selectedRows: []
246
148
  }
247
149
  },
@@ -250,75 +152,44 @@ export default {
250
152
  showPagination() {
251
153
  return this.pagination && this.total > 0
252
154
  },
253
-
254
155
  computedHeight() {
255
- if (this.virtualScroll) {
256
- return this.height || this.maxHeight || 400
257
- }
258
156
  return this.height || undefined
259
157
  },
260
-
261
158
  computedMaxHeight() {
262
- if (this.virtualScroll || this.height) return undefined
263
- return this.maxHeight || undefined
159
+ return this.height ? undefined : this.maxHeight || undefined
264
160
  },
265
161
 
266
- // 虚拟滚动固定表头占位数据(仅渲染表头,不渲染 body 行)
267
- headerTableData() {
268
- return []
269
- },
270
-
271
- // 处理后的数据(注入小计/总计行)
162
+ // 处理小计、总计后的最终数据
272
163
  processedTableData() {
273
164
  if (!this.tableData.length) return []
274
-
275
165
  let data = [...this.tableData]
276
166
  const hasSubtotal = this.subtotalConfig && this.subtotalConfig.enabled
277
167
  const hasTotal = this.totalConfig && this.totalConfig.enabled
278
168
 
279
169
  if (!hasSubtotal && !hasTotal) return data
280
170
 
281
- // 找到第一个数据列作为标签列
282
171
  const labelColumn = this.findLabelColumn()
283
-
284
172
  let result = []
285
173
 
286
174
  if (hasSubtotal && this.subtotalConfig.groupBy && this.subtotalConfig.groupBy.length) {
287
- // 按 groupBy 分组
288
175
  const groups = this.groupData(data, this.subtotalConfig.groupBy)
289
- groups.forEach((groupRows, groupKey) => {
290
- // 插入数据行
176
+ groups.forEach((groupRows) => {
291
177
  result.push(...groupRows)
292
- // 插入小计行
293
- const subtotalRow = this.createSubtotalRow(groupRows, groupKey)
178
+ const subtotalRow = this.createSubtotalRow(groupRows)
294
179
  result.push(subtotalRow)
295
180
  })
296
181
  } else {
297
182
  result = data
298
183
  }
299
184
 
300
- // 插入总计行
301
185
  if (hasTotal) {
302
186
  const totalRow = this.createTotalRow(data)
303
187
  result.push(totalRow)
304
188
  }
305
-
306
189
  return result
307
190
  },
308
191
 
309
- // 虚拟滚动总高度
310
- virtualTotalHeight() {
311
- const data = this.processedTableData
312
- return data.length * this.rowHeight
313
- },
314
-
315
- // 虚拟滚动偏移量
316
- virtualOffsetY() {
317
- const start = Math.max(0, this.visibleStartIndex - this.bufferSize)
318
- return start * this.rowHeight
319
- },
320
-
321
- // 扁平化列配置(为每级列补充 _key)
192
+ // 扁平化列配置,添加唯一 key
322
193
  flattenedColumns() {
323
194
  if (this.flattenedColumnsCache.length) return this.flattenedColumnsCache
324
195
  const assignKeys = (cols, parentPath = '') => {
@@ -334,91 +205,47 @@ export default {
334
205
  }
335
206
  this.flattenedColumnsCache = assignKeys(this.columns)
336
207
  return this.flattenedColumnsCache
337
- },
338
-
339
- // 虚拟滚动时的可见数据(附带 _realIndex 供序号列使用)
340
- visibleData() {
341
- const data = this.processedTableData
342
- if (!this.virtualScroll || data.length === 0) {
343
- return data
344
- }
345
- const start = Math.max(0, this.visibleStartIndex - this.bufferSize)
346
- const end = Math.min(data.length, this.visibleEndIndex + this.bufferSize)
347
- return data.slice(start, end).map((row, i) => ({
348
- ...row,
349
- _realIndex: start + i
350
- }))
351
208
  }
352
209
  },
353
210
 
354
211
  watch: {
355
- tableData: {
356
- handler() {
357
- this.spanCache = {}
358
- this.flattenedColumnsCache = []
359
- if (this.virtualScroll) {
360
- this.$nextTick(() => {
361
- this.measureRowHeight()
362
- this.updateVisibleRange()
363
- this.syncTableLayout()
364
- })
365
- }
366
- },
367
- immediate: false
212
+ tableData() {
213
+ this.spanCache = {}
214
+ this.flattenedColumnsCache = []
368
215
  },
369
-
370
216
  columns: {
371
217
  handler() {
372
218
  this.flattenedColumnsCache = []
373
- if (this.virtualScroll) {
374
- this.$nextTick(() => this.syncTableLayout())
375
- }
219
+ this.spanCache = {}
376
220
  },
377
221
  deep: true
378
222
  },
379
-
380
- height() {
381
- if (this.virtualScroll) {
382
- this.$nextTick(() => this.syncTableLayout())
383
- }
384
- },
385
-
386
- maxHeight() {
387
- if (this.virtualScroll) {
388
- this.$nextTick(() => this.syncTableLayout())
389
- }
390
- },
391
-
392
- virtualScroll(val) {
393
- if (val) {
394
- this.$nextTick(() => this.initVirtualScroll())
395
- } else {
396
- this.destroyVirtualScroll()
397
- }
223
+ groupColumns: {
224
+ handler() {
225
+ this.spanCache = {}
226
+ },
227
+ deep: true
398
228
  }
399
229
  },
400
230
 
401
231
  mounted() {
402
- if (this.virtualScroll) {
403
- this.$nextTick(() => this.initVirtualScroll())
404
- }
405
- // 监听窗口resize,更新表格宽度
406
- this.resizeHandler = () => {
407
- if (this.virtualScroll) {
408
- this.$nextTick(() => this.syncTableLayout())
409
- }
410
- }
411
- window.addEventListener('resize', this.resizeHandler)
232
+ this.$nextTick(() => {
233
+ window.addEventListener('resize', this.handleResize)
234
+ })
412
235
  },
413
236
 
414
237
  beforeDestroy() {
415
- this.destroyVirtualScroll()
416
- window.removeEventListener('resize', this.resizeHandler)
238
+ window.removeEventListener('resize', this.handleResize)
417
239
  },
418
240
 
419
241
  methods: {
420
- // ==================== 小计/总计工具函数 ====================
421
- // 找到第一个数据列(非 selection/index),用于显示"小计"/"总计"标签
242
+ handleResize() {
243
+ this.$nextTick(() => {
244
+ this.$refs.table && this.$refs.table.doLayout()
245
+ })
246
+ },
247
+
248
+ // ========== 小计 / 总计 相关 ==========
422
249
  findLabelColumn() {
423
250
  for (const col of this.columns) {
424
251
  if (col.children && col.children.length) {
@@ -432,33 +259,25 @@ export default {
432
259
  return { prop: '', label: '' }
433
260
  },
434
261
 
435
- // 按指定字段分组(保持分组顺序)
436
262
  groupData(data, groupBy) {
437
263
  const map = new Map()
438
264
  data.forEach(row => {
439
265
  const key = groupBy.map(f => row[f]).join('|||')
440
- if (!map.has(key)) {
441
- map.set(key, [])
442
- }
266
+ if (!map.has(key)) map.set(key, [])
443
267
  map.get(key).push(row)
444
268
  })
445
269
  return map
446
270
  },
447
271
 
448
- // 计算单列的聚合值
449
272
  _calcValue(rows, calc) {
450
- // 自定义函数
451
273
  if (typeof calc === 'function') return calc(rows)
452
-
453
- // 字符串:'sum' | 'avg' | 'count' | 'min' | 'max'(需配合 prop 使用)
454
-
455
- // 对象:{ prop: 'field', type: 'sum' } 或 { prop: 'field' } (默认 sum)
456
274
  const prop = calc.prop
457
275
  const type = calc.type || 'sum'
458
276
  const vals = prop ? rows.map(r => parseFloat(r[prop]) || 0) : []
459
277
  switch (type) {
460
278
  case 'sum': return vals.reduce((s, v) => s + v, 0)
461
- case 'avg': case 'average': return vals.length ? (vals.reduce((s, v) => s + v, 0) / vals.length) : 0
279
+ case 'avg':
280
+ case 'average': return vals.length ? vals.reduce((s, v) => s + v, 0) / vals.length : 0
462
281
  case 'count': return rows.length
463
282
  case 'min': return vals.length ? Math.min(...vals) : 0
464
283
  case 'max': return vals.length ? Math.max(...vals) : 0
@@ -466,20 +285,16 @@ export default {
466
285
  }
467
286
  },
468
287
 
469
- // 创建小计行
470
- createSubtotalRow(groupRows, groupKey) {
288
+ createSubtotalRow(groupRows) {
471
289
  const config = this.subtotalConfig
472
290
  const rawLabelProp = this.findLabelColumn()
473
-
474
- // 获取第一个 groupBy 列的值显示
475
291
  const labelField = config.groupBy ? config.groupBy[0] : ''
476
- const groupValue = groupRows[0] ? groupRows[0][labelField] : groupKey
292
+ const groupValue = groupRows[0] ? groupRows[0][labelField] : ''
477
293
  const labelText = config.labelText || `${groupValue} 小计`
478
294
 
479
- const row = { _rowType: 'subtotal', _groupKey: groupKey }
295
+ const row = { _rowType: 'subtotal' }
480
296
  row[rawLabelProp.prop] = labelText
481
297
 
482
- // 计算各列的聚合值
483
298
  if (config.columns) {
484
299
  Object.keys(config.columns).forEach(prop => {
485
300
  const calc = config.columns[prop]
@@ -488,11 +303,9 @@ export default {
488
303
  : this._calcValue(groupRows, calc)
489
304
  })
490
305
  }
491
-
492
306
  return row
493
307
  },
494
308
 
495
- // 创建总计行
496
309
  createTotalRow(allRows) {
497
310
  const config = this.totalConfig
498
311
  const rawLabelProp = this.findLabelColumn()
@@ -501,7 +314,6 @@ export default {
501
314
  const row = { _rowType: 'total' }
502
315
  row[rawLabelProp.prop] = labelText
503
316
 
504
- // 计算各列的聚合值
505
317
  if (config.columns) {
506
318
  Object.keys(config.columns).forEach(prop => {
507
319
  const calc = config.columns[prop]
@@ -510,55 +322,40 @@ export default {
510
322
  : this._calcValue(allRows, calc)
511
323
  })
512
324
  }
513
-
514
325
  return row
515
326
  },
516
327
 
517
- // 行类名(小计/总计行样式)
518
328
  getRowClassName({ row }) {
519
- if (!row) return ''
520
329
  if (row._rowType === 'subtotal') return 'ex-table-row-subtotal'
521
330
  if (row._rowType === 'total') return 'ex-table-row-total'
522
331
  return ''
523
332
  },
524
333
 
525
- // ==================== 列配置 ====================
334
+ // ========== 列处理 ==========
526
335
  getColumnProps(col) {
527
336
  const { _key, children, render, formatter, slot, ...props } = col
528
337
  return props
529
338
  },
530
339
 
531
- // ==================== 序号方法 ====================
532
340
  indexMethod(index) {
533
- if (this.virtualScroll && this.visibleData.length > 0) {
534
- const row = this.visibleData[index]
535
- if (row && row._realIndex !== undefined) {
536
- return row._realIndex + 1
537
- }
538
- }
539
341
  return index + 1
540
342
  },
541
343
 
542
- // ==================== 分组合并 ====================
344
+ // ========== 合并单元格 ==========
543
345
  handleSpanMethod({ row, column, rowIndex }) {
544
- // 跳过小计/总计行,不做合并
545
346
  if (row._rowType === 'subtotal' || row._rowType === 'total') {
546
347
  return { rowspan: 1, colspan: 1 }
547
348
  }
548
-
549
349
  if (!this.groupColumns.length) return { rowspan: 1, colspan: 1 }
550
350
 
551
- const data = this.virtualScroll ? this.visibleData : this.processedTableData
351
+ const data = this.processedTableData
552
352
  const prop = column.property
553
353
  const groupIndex = this.groupColumns.indexOf(prop)
554
-
555
354
  if (groupIndex === -1) return { rowspan: 1, colspan: 1 }
556
355
 
557
- // 检查缓存
558
356
  const cacheKey = `${rowIndex}_${prop}`
559
357
  if (this.spanCache[cacheKey]) return this.spanCache[cacheKey]
560
358
 
561
- // 判断当前行是否与前一行在分组列上完全相同
562
359
  if (rowIndex > 0) {
563
360
  const prevRow = data[rowIndex - 1]
564
361
  let isSame = true
@@ -570,185 +367,76 @@ export default {
570
367
  }
571
368
  }
572
369
  if (isSame) {
573
- const result = { rowspan: 0, colspan: 1 }
574
- this.spanCache[cacheKey] = result
575
- return result
370
+ const res = { rowspan: 0, colspan: 1 }
371
+ this.spanCache[cacheKey] = res
372
+ return res
576
373
  }
577
374
  }
578
375
 
579
- // 向下扫描计算合并行数
580
376
  let count = 1
581
377
  for (let i = rowIndex + 1; i < data.length; i++) {
582
378
  const nextRow = data[i]
583
- let isSame = true
379
+ let same = true
584
380
  for (let j = 0; j <= groupIndex; j++) {
585
381
  const gp = this.groupColumns[j]
586
382
  if (nextRow[gp] !== row[gp]) {
587
- isSame = false
383
+ same = false
588
384
  break
589
385
  }
590
386
  }
591
- if (isSame) {
592
- count++
593
- } else {
594
- break
595
- }
387
+ if (same) count++
388
+ else break
596
389
  }
597
390
 
598
- const result = { rowspan: count > 1 ? count : 1, colspan: 1 }
599
- this.spanCache[cacheKey] = result
600
- return result
391
+ const res = { rowspan: count > 1 ? count : 1, colspan: 1 }
392
+ this.spanCache[cacheKey] = res
393
+ return res
601
394
  },
602
395
 
603
- // ==================== 选择 ====================
396
+ // ========== 选择、排序、分页 ==========
604
397
  handleSelectionChange(rows) {
605
- // 过滤掉小计/总计行
606
398
  this.selectedRows = rows.filter(r => !r._rowType)
607
399
  this.$emit('selection-change', this.selectedRows)
608
400
  },
609
-
610
- // ==================== 排序 ====================
611
- handleSortChange({ prop, order }) {
612
- this.$emit('sort-change', { prop, order })
401
+ handleSortChange(info) {
402
+ this.$emit('sort-change', info)
613
403
  },
614
-
615
- // ==================== 分页 ====================
616
404
  handleSizeChange(size) {
617
405
  this.$emit('size-change', size)
618
406
  },
619
-
620
407
  handleCurrentChange(page) {
621
408
  this.$emit('page-change', page)
622
409
  },
623
410
 
624
- // ==================== 公开方法 ====================
411
+ // ========== 对外暴露方法 ==========
625
412
  getSelection() {
626
413
  return this.selectedRows
627
414
  },
628
-
629
415
  clearSelection() {
630
416
  this.$refs.table && this.$refs.table.clearSelection()
631
417
  this.selectedRows = []
632
418
  },
633
-
634
419
  toggleRowSelection(row, selected) {
635
420
  this.$refs.table && this.$refs.table.toggleRowSelection(row, selected)
636
421
  },
637
-
638
422
  toggleRowsSelection(rows, selected) {
639
- if (rows && rows.length) {
640
- rows.forEach(row => {
641
- this.$refs.table && this.$refs.table.toggleRowSelection(row, selected)
642
- })
643
- }
423
+ if (!rows || !rows.length) return
424
+ rows.forEach(row => {
425
+ this.$refs.table && this.$refs.table.toggleRowSelection(row, selected)
426
+ })
644
427
  },
645
-
646
428
  doLayout() {
647
- this.syncTableLayout()
648
- },
649
-
650
- // ==================== 虚拟滚动固定表头 ====================
651
- syncTableLayout() {
652
- const bodyTable = this.$refs.table
653
- const headerTable = this.$refs.headerTable
654
- if (bodyTable) bodyTable.doLayout()
655
- if (headerTable) headerTable.doLayout()
656
- this.syncHeaderScroll(this.scrollLeft)
657
- },
658
-
659
- syncHeaderScroll(scrollLeft) {
660
- const headerTable = this.$refs.headerTable
661
- if (!headerTable) return
662
- const headerWrapper = headerTable.$el.querySelector('.el-table__header-wrapper')
663
- if (headerWrapper) {
664
- headerWrapper.scrollLeft = scrollLeft
665
- }
666
- },
667
-
668
- // ==================== 虚拟滚动 ====================
669
- initVirtualScroll() {
670
- this.rowHeight = this.rowInitHeight
671
- this.measureRowHeight()
672
- if (this.processedTableData.length > 0) {
673
- this.updateVisibleRange()
674
- }
675
- this.syncTableLayout()
676
- },
677
-
678
- measureRowHeight() {
679
- const el = this.$refs.table && this.$refs.table.$el
680
- if (!el) return
681
- const row = el.querySelector('.el-table__row')
682
- if (row) {
683
- this.rowHeight = row.offsetHeight || this.rowInitHeight
684
- }
685
- },
686
-
687
- handleBodyScroll(event) {
688
- const target = event.target
689
- this.scrollLeft = target.scrollLeft
690
- if (this.virtualScroll) {
691
- this.scrollTop = target.scrollTop
692
- this.updateVisibleRange()
693
- this.syncHeaderScroll(target.scrollLeft)
694
- }
695
- },
696
-
697
- updateVisibleRange() {
698
- const totalCount = this.processedTableData.length
699
- if (totalCount === 0 || this.rowHeight === 0) return
700
-
701
- const visibleCount = Math.ceil((this.computedHeight || 400) / this.rowHeight)
702
-
703
- this.visibleStartIndex = Math.max(0, Math.floor(this.scrollTop / this.rowHeight))
704
- this.visibleEndIndex = Math.min(
705
- totalCount,
706
- this.visibleStartIndex + visibleCount
707
- )
708
- this.visibleCount = visibleCount
709
- },
710
-
711
- destroyVirtualScroll() {
712
- this.scrollTop = 0
713
- this.visibleStartIndex = 0
714
- this.visibleEndIndex = 0
429
+ this.$refs.table && this.$refs.table.doLayout()
715
430
  }
716
431
  }
717
432
  }
718
433
  </script>
719
-
720
434
  <style scoped>
721
435
  .ex-table-wrapper {
722
436
  width: 100%;
723
437
  }
724
438
 
725
- /* 小计行样式 */
726
- .ex-table-wrapper >>> .ex-table-row-subtotal,
727
- .ex-table >>> .ex-table-row-subtotal {
728
- background-color: #f5f7fa;
729
- font-weight: 600;
730
- color: #303133;
731
- }
732
-
733
- /* 总计行样式 */
734
- .ex-table-wrapper >>> .ex-table-row-total,
735
- .ex-table >>> .ex-table-row-total {
736
- background-color: #e8eaed;
737
- font-weight: 700;
738
- color: #303133;
739
- border-top: 2px solid #c0c4cc;
740
- }
741
-
742
- .ex-table-wrapper >>> .ex-table-row-subtotal:hover > td,
743
- .ex-table >>> .ex-table-row-subtotal:hover > td {
744
- background-color: #ebeef5 !important;
745
- }
746
-
747
- .ex-table-wrapper >>> .ex-table-row-total:hover > td,
748
- .ex-table >>> .ex-table-row-total:hover > td {
749
- background-color: #dcdfe6 !important;
750
- }
751
-
439
+ /* 表头栏 */
752
440
  .ex-table-header {
753
441
  display: flex;
754
442
  align-items: center;
@@ -756,7 +444,6 @@ export default {
756
444
  padding: 12px 0;
757
445
  margin-bottom: 8px;
758
446
  }
759
-
760
447
  .ex-table-title {
761
448
  font-size: 16px;
762
449
  font-weight: 600;
@@ -764,7 +451,6 @@ export default {
764
451
  position: relative;
765
452
  padding-left: 12px;
766
453
  }
767
-
768
454
  .ex-table-title::before {
769
455
  content: '';
770
456
  position: absolute;
@@ -776,7 +462,6 @@ export default {
776
462
  background: #409eff;
777
463
  border-radius: 2px;
778
464
  }
779
-
780
465
  .ex-table-toolbar {
781
466
  display: flex;
782
467
  align-items: center;
@@ -785,52 +470,98 @@ export default {
785
470
 
786
471
  .ex-table-body {
787
472
  position: relative;
473
+ z-index: 1;
788
474
  }
789
475
 
790
- /* 虚拟滚动容器 */
791
- .ex-table-virtual {
792
- overflow: overlay;
793
- position: relative;
794
- /* Firefox 隐藏滚动条 */
795
- scrollbar-width: none;
796
- /* IE/旧 Edge 隐藏滚动条 */
797
- -ms-overflow-style: none;
476
+ /* 分页区域 */
477
+ .ex-table-footer {
478
+ display: flex;
479
+ justify-content: flex-end;
480
+ padding: 16px 0;
798
481
  }
799
482
 
800
- .ex-table-virtual-inner {
801
- position: relative;
802
- width: 100%;
483
+ /* ========== 小计/合计行样式 ========== */
484
+ ::v-deep .ex-table-row-subtotal {
485
+ background-color: #f5f7fa;
486
+ font-weight: 600;
487
+ color: #303133;
803
488
  }
804
-
805
- .ex-table-virtual-inner > div {
806
- will-change: transform;
489
+ ::v-deep .ex-table-row-total {
490
+ background-color: #e8eaed;
491
+ font-weight: 700;
492
+ color: #303133;
493
+ border-top: 2px solid #c0c4cc;
494
+ }
495
+ ::v-deep .ex-table-row-subtotal:hover > td {
496
+ background-color: #ebeef5 !important;
497
+ }
498
+ ::v-deep .ex-table-row-total:hover > td {
499
+ background-color: #dcdfe6 !important;
807
500
  }
808
501
 
809
- .ex-table-footer {
810
- display: flex;
811
- justify-content: flex-end;
812
- padding: 16px 0;
502
+ /* ========== 滚动条:Y轴 + X轴 统一处理 ========== */
503
+ /* 合并为一个选择器块,避免重复导致的优先级问题 */
504
+ ::v-deep .el-table__body-wrapper {
505
+ overflow-y: auto !important;
506
+ overflow-x: auto !important;
507
+ user-select: none;
508
+ -webkit-user-select: none;
509
+ }
510
+ ::v-deep .el-table__body-wrapper::-webkit-scrollbar {
511
+ width: 8px;
512
+ height: 8px;
513
+ }
514
+ ::v-deep .el-table__body-wrapper::-webkit-scrollbar-thumb {
515
+ background: #c1c1c1;
516
+ border-radius: 4px;
517
+ }
518
+ ::v-deep .el-table__body-wrapper::-webkit-scrollbar-track {
519
+ background: #f1f1f1;
520
+ }
521
+ /* 滚动条角落(Y+X 交汇处) */
522
+ ::v-deep .el-table__body-wrapper::-webkit-scrollbar-corner {
523
+ background: #f1f1f1;
813
524
  }
814
525
 
815
- /* 虚拟滚动固定表头(header-only el-table) */
816
- .ex-table-fixed-header {
817
- overflow: hidden;
818
- flex-shrink: 0;
526
+ /* 允许单元格内文字选中,但容器本身不可选中 */
527
+ ::v-deep .el-table__body-wrapper .el-table__cell {
528
+ user-select: text;
529
+ -webkit-user-select: text;
819
530
  }
820
531
 
821
- /* 虚拟滚动表头强制补上 gutter 占位 */
822
- .ex-table-fixed-header >>> .el-table__header-wrapper thead tr .el-table__cell.gutter {
823
- display: table-cell;
824
- width: 8px; /* 和滚动条宽度一致 */
532
+ /* ========== 固定列层级 + 背景 防文字穿透 ========== */
533
+ ::v-deep .el-table__fixed {
534
+ z-index: 10 !important;
535
+ box-shadow: 4px 0 8px rgba(0, 0, 0, 0.08);
536
+ }
537
+ ::v-deep .el-table__fixed-right {
538
+ z-index: 10 !important;
539
+ box-shadow: -4px 0 8px rgba(0, 0, 0, 0.08);
825
540
  }
826
541
 
542
+ /* 固定列单元格底色(纯白兜底,盖住滚动穿透文字) */
543
+ ::v-deep .el-table__fixed .el-table__cell,
544
+ ::v-deep .el-table__fixed-right .el-table__cell {
545
+ background-color: #ffffff !important;
546
+ }
547
+ ::v-deep .el-table__fixed .el-table__row:nth-child(even) .el-table__cell,
548
+ ::v-deep .el-table__fixed-right .el-table__row:nth-child(even) .el-table__cell {
549
+ background-color: #fafafa !important;
550
+ }
551
+ ::v-deep .el-table__fixed .el-table__row:hover .el-table__cell,
552
+ ::v-deep .el-table__fixed-right .el-table__row:hover .el-table__cell {
553
+ background-color: #f5f7fa !important;
554
+ }
827
555
 
828
- .ex-table-header-only >>> .el-table__body-wrapper,
829
- .ex-table-header-only >>> .el-table__empty-block {
830
- display: none;
556
+ ::v-deep .el-table__fixed-header-wrapper,
557
+ ::v-deep .el-table__fixed-right-header-wrapper {
558
+ z-index: 11 !important;
831
559
  }
832
560
 
833
- .ex-table-header-only >>> .el-table__header-wrapper {
834
- overflow: hidden;
561
+ /* ========== 虚拟滚动容器:正常布局流撑开滚动条 ========== */
562
+ ::v-deep .vs-phantom {
563
+ position: relative;
564
+ box-sizing: border-box;
565
+ z-index: 1 !important;
835
566
  }
836
- </style>
567
+ </style>