xt-element-ui 1.2.1 → 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.
Files changed (32) hide show
  1. package/lib/index.common.js +1306 -1398
  2. package/lib/index.css +1 -1
  3. package/lib/index.umd.js +1311 -1403
  4. package/lib/index.umd.min.js +5 -5
  5. package/package.json +3 -4
  6. package/src/components/ex-button/index.vue +8 -2
  7. package/src/components/ex-button/style/index.scss +42 -6
  8. package/src/components/ex-card/index.vue +0 -82
  9. package/src/components/ex-card/style/index.scss +7 -3
  10. package/src/components/ex-chart/theme/dark.js +1 -1
  11. package/src/components/ex-chart/utils.js +1 -1
  12. package/src/components/ex-page/index.js +2 -0
  13. package/src/components/ex-select-tree/index.js +2 -0
  14. package/src/components/{xt-select-tree → ex-select-tree}/index.vue +1 -1
  15. package/src/components/ex-table/ExTableCell.vue +3 -2
  16. package/src/components/ex-table/VirtualElTable.vue +234 -0
  17. package/src/components/ex-table/index.vue +188 -625
  18. package/src/components/ex-table/virtualScrollData.js +35 -0
  19. package/src/components/ex-upload/index.js +2 -0
  20. package/src/components/xt-config-provider/index.js +2 -2
  21. package/src/components/xt-config-provider/index.vue +17 -13
  22. package/src/components/xt-text/style/index.scss +13 -13
  23. package/src/index.js +4 -1
  24. package/src/styles/css-variables.scss +1 -2
  25. package/src/styles/element-theme.scss +12 -4
  26. package/src/utils/index.js +11 -2
  27. package/src/components/xt-page/index.js +0 -0
  28. package/src/components/xt-select-tree/index.js +0 -0
  29. package/src/components/xt-upload/index.js +0 -2
  30. /package/src/components/{xt-page → ex-page}/index.vue +0 -0
  31. /package/src/components/{xt-upload → ex-upload}/index.vue +0 -0
  32. /package/src/components/{xt-upload → ex-upload}/preview.vue +0 -0
@@ -8,104 +8,16 @@
8
8
  </div>
9
9
  </div>
10
10
 
11
- <!-- 虚拟滚动固定表头(独立于滚动容器) -->
12
- <div
13
- v-if="virtualScroll && headerRows.length"
14
- ref="fixedHeader"
15
- class="ex-table-fixed-header"
16
- :style="{ marginLeft: -scrollLeft + 'px' }"
17
- >
18
- <table class="ex-table-fixed-header-table">
19
- <colgroup>
20
- <col v-for="col in headerCols" :key="col._key" :name="col._key" :width="col._width" :min-width="col._minWidth" />
21
- <col name="gutter" width="12" />
22
- </colgroup>
23
- <thead>
24
- <tr v-for="(row, rowIdx) in headerRows" :key="rowIdx">
25
- <template v-for="cell in row">
26
- <th
27
- v-if="!cell._hidden"
28
- :key="cell._key"
29
- :rowspan="cell._rowspan"
30
- :colspan="cell._colspan"
31
- :style="cell._style || {}"
32
- >{{ cell.label }}</th>
33
- </template>
34
- </tr>
35
- </thead>
36
- </table>
37
- </div>
38
-
39
- <!-- 表格主体 -->
40
- <div
41
- class="ex-table-body"
42
- :class="{ 'ex-table-virtual': virtualScroll }"
43
- :style="virtualScroll ? { height: computedHeight + 'px' } : {}"
44
- ref="tableBody"
45
- @scroll="handleBodyScroll"
46
- >
47
- <!-- 虚拟滚动:包裹层 + 占位撑开 -->
48
- <div v-if="virtualScroll" class="ex-table-virtual-inner" :style="{ height: virtualTotalHeight + 'px' }">
49
- <div :style="{ transform: 'translateY(' + virtualOffsetY + 'px)' }">
50
- <el-table
51
- ref="table"
52
- :data="visibleData"
53
- :span-method="groupColumns.length ? handleSpanMethod : undefined"
54
- :row-class-name="getRowClassName"
55
- :show-header="false"
56
- v-bind="$attrs"
57
- v-on="$listeners"
58
- @selection-change="handleSelectionChange"
59
- @sort-change="handleSortChange"
60
- class="ex-table"
61
- >
62
- <el-table-column v-if="selection" type="selection" width="55" :fixed="selectionFixed" />
63
- <el-table-column
64
- v-if="showIndex"
65
- type="index"
66
- width="60"
67
- label="#"
68
- :fixed="indexFixed"
69
- :index="indexMethod"
70
- />
71
- <template v-for="col in flattenedColumns">
72
- <el-table-column
73
- v-if="col.children && col.children.length"
74
- :key="col._key"
75
- v-bind="getColumnProps(col)"
76
- >
77
- <template v-for="child in col.children">
78
- <el-table-column :key="child._key" v-bind="getColumnProps(child)">
79
- <template v-if="child.render" v-slot="scope">{{ child.render(scope) }}</template>
80
- <template v-else-if="child.formatter" v-slot="scope">
81
- <ExTableCell :row="scope.row" :index="scope.$index" :formatter="child.formatter" :column="child" />
82
- </template>
83
- <template v-else-if="child.slot" v-slot="scope">
84
- <slot :name="child.slot" :row="scope.row" :index="scope.$index" :column="child" />
85
- </template>
86
- </el-table-column>
87
- </template>
88
- </el-table-column>
89
- <el-table-column v-else :key="col._key" v-bind="getColumnProps(col)">
90
- <template v-if="col.render" v-slot="scope">{{ col.render(scope) }}</template>
91
- <template v-else-if="col.formatter" v-slot="scope">
92
- <ExTableCell :row="scope.row" :index="scope.$index" :formatter="col.formatter" :column="col" />
93
- </template>
94
- <template v-else-if="col.slot" v-slot="scope">
95
- <slot :name="col.slot" :row="scope.row" :index="scope.$index" :column="col" />
96
- </template>
97
- </el-table-column>
98
- </template>
99
- </el-table>
100
- </div>
101
- </div>
102
-
103
- <!-- 非虚拟滚动:直接渲染 -->
104
- <el-table
105
- v-else
11
+ <!-- 主体表格:单 Table + 内置虚拟滚动(核心改造) -->
12
+ <div class="ex-table-body">
13
+ <VirtualElTable
106
14
  ref="table"
107
- :data="visibleData"
15
+ :data="processedTableData"
108
16
  :height="computedHeight"
17
+ :max-height="computedMaxHeight"
18
+ :virtual-scroll="virtualScroll"
19
+ :row-height="rowInitHeight"
20
+ :buffer-size="bufferSize"
109
21
  :span-method="groupColumns.length ? handleSpanMethod : undefined"
110
22
  :row-class-name="getRowClassName"
111
23
  v-bind="$attrs"
@@ -114,7 +26,14 @@
114
26
  @sort-change="handleSortChange"
115
27
  class="ex-table"
116
28
  >
117
- <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
+ <!-- 序号列 -->
118
37
  <el-table-column
119
38
  v-if="showIndex"
120
39
  type="index"
@@ -123,6 +42,7 @@
123
42
  :fixed="indexFixed"
124
43
  :index="indexMethod"
125
44
  />
45
+ <!-- 多级列配置 -->
126
46
  <template v-for="col in flattenedColumns">
127
47
  <el-table-column
128
48
  v-if="col.children && col.children.length"
@@ -133,10 +53,20 @@
133
53
  <el-table-column :key="child._key" v-bind="getColumnProps(child)">
134
54
  <template v-if="child.render" v-slot="scope">{{ child.render(scope) }}</template>
135
55
  <template v-else-if="child.formatter" v-slot="scope">
136
- <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
+ />
137
62
  </template>
138
63
  <template v-else-if="child.slot" v-slot="scope">
139
- <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
+ />
140
70
  </template>
141
71
  </el-table-column>
142
72
  </template>
@@ -144,14 +74,24 @@
144
74
  <el-table-column v-else :key="col._key" v-bind="getColumnProps(col)">
145
75
  <template v-if="col.render" v-slot="scope">{{ col.render(scope) }}</template>
146
76
  <template v-else-if="col.formatter" v-slot="scope">
147
- <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
+ />
148
83
  </template>
149
84
  <template v-else-if="col.slot" v-slot="scope">
150
- <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
+ />
151
91
  </template>
152
92
  </el-table-column>
153
93
  </template>
154
- </el-table>
94
+ </VirtualElTable>
155
95
  </div>
156
96
 
157
97
  <!-- 分页 -->
@@ -171,68 +111,39 @@
171
111
 
172
112
  <script>
173
113
  import ExTableCell from './ExTableCell.vue'
174
- // 内部导入 element-ui 组件,避免与外部 element-ui 冲突
175
- import { Table as ElTable, TableColumn as ElTableColumn } from 'element-ui'
114
+ import VirtualElTable from './VirtualElTable.vue'
176
115
 
177
116
  export default {
178
117
  name: 'ExTable',
179
118
  inheritAttrs: false,
180
- components: { ExTableCell, ElTable, ElTableColumn },
119
+ components: { ExTableCell, VirtualElTable },
120
+
181
121
  props: {
182
- // 表格数据
183
122
  tableData: { type: Array, default: () => [] },
184
- // 列配置(支持 children 多级表头)
185
123
  columns: { type: Array, default: () => [] },
186
- // 分组列配置(按配置的层级合并单元格)
187
124
  groupColumns: { type: Array, default: () => [] },
188
- // 标题
189
125
  title: { type: String, default: '' },
190
- // 高度
191
126
  height: { type: [Number, String], default: null },
192
- // 虚拟滚动
127
+ maxHeight: { type: [Number, String], default: null },
193
128
  virtualScroll: { type: Boolean, default: false },
194
- // 预估行高
195
129
  rowInitHeight: { type: Number, default: 48 },
196
- // 缓冲区行数
197
130
  bufferSize: { type: Number, default: 5 },
198
- // 分页配置
199
131
  pagination: { type: Object, default: null },
200
- // 总条数
201
132
  total: { type: Number, default: 0 },
202
- // 是否显示序号
203
133
  showIndex: { type: Boolean, default: false },
204
- // 是否可选
205
134
  selection: { type: Boolean, default: false },
206
- // 选择列固定位置
207
135
  selectionFixed: { type: [String, Boolean], default: false },
208
- // 序号列固定位置
209
136
  indexFixed: { type: [String, Boolean], default: false },
210
- // 加载状态
211
137
  loading: { type: Boolean, default: false },
212
- // 空数据文案
213
138
  emptyText: { type: String, default: '暂无数据' },
214
- // 小计配置
215
139
  subtotalConfig: { type: Object, default: () => ({ enabled: false }) },
216
- // 总计配置
217
140
  totalConfig: { type: Object, default: () => ({ enabled: false }) }
218
141
  },
219
142
 
220
143
  data() {
221
144
  return {
222
- // 虚拟滚动相关
223
- scrollTop: 0,
224
- scrollLeft: 0,
225
- rowHeight: 48,
226
- visibleStartIndex: 0,
227
- visibleEndIndex: 0,
228
- visibleCount: 0,
229
- // 表格宽度(用于自适应列宽计算)
230
- tableWidth: 0,
231
- // 分组合并缓存
232
145
  spanCache: {},
233
- // 列扁平化缓存
234
146
  flattenedColumnsCache: [],
235
- // 选中数据
236
147
  selectedRows: []
237
148
  }
238
149
  },
@@ -241,337 +152,100 @@ export default {
241
152
  showPagination() {
242
153
  return this.pagination && this.total > 0
243
154
  },
244
-
245
155
  computedHeight() {
246
- if (this.virtualScroll) {
247
- return this.height || 400
248
- }
249
156
  return this.height || undefined
250
157
  },
251
-
252
- // 计算固定列总宽度(selection + index + fixed列)
253
- fixedColumnWidth() {
254
- let width = 0
255
- // 选择列
256
- if (this.selection) width += 55
257
- // 序号列
258
- if (this.showIndex) width += 60
259
- // fixed 列
260
- const sumFixedWidth = (cols) => {
261
- cols.forEach(c => {
262
- if (c.fixed && (c.width || c.minWidth)) {
263
- width += typeof c.width === 'number' ? c.width : (typeof c.minWidth === 'number' ? c.minWidth : 100)
264
- }
265
- if (c.children && c.children.length) sumFixedWidth(c.children)
266
- })
267
- }
268
- sumFixedWidth(this.columns)
269
- return width
270
- },
271
-
272
- // 计算非固定列中已设置width的总宽度
273
- nonFixedColumnWidthWithWidth() {
274
- let width = 0
275
- const sumWidth = (cols) => {
276
- cols.forEach(c => {
277
- if (!c.fixed && c.width) {
278
- width += typeof c.width === 'number' ? c.width : parseInt(c.width) || 0
279
- }
280
- if (c.children && c.children.length) sumWidth(c.children)
281
- })
282
- }
283
- sumWidth(this.columns)
284
- return width
158
+ computedMaxHeight() {
159
+ return this.height ? undefined : this.maxHeight || undefined
285
160
  },
286
161
 
287
- // 计算需要自适应的列数(非固定列且未设置width)
288
- autoColumnCount() {
289
- let count = 0
290
- const countCols = (cols) => {
291
- cols.forEach(c => {
292
- if (!c.fixed) {
293
- if (c.children && c.children.length) {
294
- countCols(c.children)
295
- } else if (!c.width) {
296
- // 只有未设置width的非固定列才参与自适应
297
- count++
298
- }
299
- }
300
- })
301
- }
302
- countCols(this.columns)
303
- return count
304
- },
305
-
306
- // 自适应列宽(未设置width的非固定列均分剩余宽度)
307
- autoColumnWidth() {
308
- const totalWidth = this.tableWidth || 1000
309
- const gutter = 12 // el-table 默认 gutter
310
- const fixedWidth = this.fixedColumnWidth
311
- const nonFixedWithWidth = this.nonFixedColumnWidthWithWidth
312
- const autoCount = this.autoColumnCount
313
-
314
- if (autoCount <= 0) return 100 // 默认宽度
315
-
316
- // 自适应宽度 = (总宽度 - gutter - 固定列宽度 - 非固定列中已设置的width)/ 需要自适应的列数
317
- const availableWidth = totalWidth - gutter - fixedWidth - nonFixedWithWidth
318
- return Math.max(80, Math.floor(availableWidth / autoCount)) // 最小80px
319
- },
320
-
321
- // 表头列定义(用于 colgroup,与 headerRows 列数保持一致)
322
- headerCols() {
323
- const cols = []
324
-
325
- // 选择列
326
- if (this.selection) {
327
- cols.push({ _key: 'prepend_selection', _width: 55, _minWidth: 55 })
328
- }
329
-
330
- // 序号列
331
- if (this.showIndex) {
332
- cols.push({ _key: 'prepend_index', _width: 60, _minWidth: 60 })
333
- }
334
-
335
- // 数据列(只取叶子节点,使用一致的key生成)
336
- const addLeafCols = (columns, parentPath = '') => {
337
- columns.forEach((col, index) => {
338
- const path = parentPath ? `${parentPath}_${index}` : String(index)
339
- const key = this.generateColKey(col, path)
340
-
341
- if (col.children && col.children.length) {
342
- addLeafCols(col.children, path)
343
- } else {
344
- cols.push({
345
- _key: key,
346
- _width: this.getColumnWidth(col),
347
- _minWidth: col.minWidth
348
- })
349
- }
350
- })
351
- }
352
- addLeafCols(this.columns)
353
-
354
- return cols
355
- },
356
-
357
- // 表头层级结构(用于自定义固定表头渲染)
358
- headerRows() {
359
- const rows = []
360
- let maxDepth = 1
361
-
362
- // 计算最大深度
363
- const calcDepth = (cols, depth) => {
364
- maxDepth = Math.max(maxDepth, depth)
365
- cols.forEach(c => { if (c.children && c.children.length) calcDepth(c.children, depth + 1) })
366
- }
367
- if (this.columns.length) calcDepth(this.columns, 1)
368
-
369
- // 初始化行
370
- for (let i = 0; i < maxDepth; i++) rows.push([])
371
-
372
- // 计算叶子数量
373
- const countLeaves = (cols) => {
374
- let n = 0
375
- cols.forEach(c => {
376
- if (c.children && c.children.length) n += countLeaves(c.children)
377
- else n++
378
- })
379
- return n
380
- }
381
-
382
- // 选择列 / 序号列占位(仅第一行,rowspan 跨所有行)
383
- if (this.selection) {
384
- rows[0].push({
385
- _key: 'prepend_selection',
386
- _rowspan: maxDepth,
387
- _colspan: 1,
388
- _style: { width: '55px' },
389
- _hidden: false,
390
- label: ''
391
- })
392
- for (let i = 1; i < maxDepth; i++) {
393
- rows[i].push({ _key: 'prepend_selection_' + i, _rowspan: 0, _colspan: 1, _hidden: true, label: '' })
394
- }
395
- }
396
- if (this.showIndex) {
397
- rows[0].push({
398
- _key: 'prepend_index',
399
- _rowspan: maxDepth,
400
- _colspan: 1,
401
- _style: { width: '60px' },
402
- _hidden: false,
403
- label: '#'
404
- })
405
- for (let i = 1; i < maxDepth; i++) {
406
- rows[i].push({ _key: 'prepend_index_' + i, _rowspan: 0, _colspan: 1, _hidden: true, label: '' })
407
- }
408
- }
409
-
410
- // 填充列
411
- if (this.columns.length) {
412
- const fill = (cols, depth, parentPath = '') => {
413
- cols.forEach((c, index) => {
414
- const path = parentPath ? `${parentPath}_${index}` : String(index)
415
- const key = this.generateColKey(c, path)
416
- const hasKids = c.children && c.children.length
417
- const style = {}
418
-
419
- // 如果有固定宽度,使用固定宽度;否则使用自适应宽度
420
- if (c.width) {
421
- style.width = typeof c.width === 'number' ? c.width + 'px' : c.width
422
- } else if (!c.fixed && this.virtualScroll) {
423
- // 非固定列且虚拟滚动时使用自适应宽度
424
- style.width = this.autoColumnWidth + 'px'
425
- }
426
-
427
- if (c.minWidth) style.minWidth = typeof c.minWidth === 'number' ? c.minWidth + 'px' : c.minWidth
428
-
429
- rows[depth].push({
430
- _key: key,
431
- _rowspan: hasKids ? 1 : maxDepth - depth,
432
- _colspan: hasKids ? countLeaves(c.children) : 1,
433
- _style: style,
434
- _hidden: !c.label && !c.prop,
435
- label: c.label || ''
436
- })
437
- if (hasKids) fill(c.children, depth + 1, path)
438
- })
439
- }
440
- fill(this.columns, 0)
441
- }
442
-
443
- return rows
444
- },
445
-
446
- // 处理后的数据(注入小计/总计行)
162
+ // 处理小计、总计后的最终数据
447
163
  processedTableData() {
448
164
  if (!this.tableData.length) return []
449
-
450
165
  let data = [...this.tableData]
451
166
  const hasSubtotal = this.subtotalConfig && this.subtotalConfig.enabled
452
167
  const hasTotal = this.totalConfig && this.totalConfig.enabled
453
168
 
454
169
  if (!hasSubtotal && !hasTotal) return data
455
170
 
456
- // 找到第一个数据列作为标签列
457
171
  const labelColumn = this.findLabelColumn()
458
-
459
172
  let result = []
460
173
 
461
174
  if (hasSubtotal && this.subtotalConfig.groupBy && this.subtotalConfig.groupBy.length) {
462
- // 按 groupBy 分组
463
175
  const groups = this.groupData(data, this.subtotalConfig.groupBy)
464
- groups.forEach((groupRows, groupKey) => {
465
- // 插入数据行
176
+ groups.forEach((groupRows) => {
466
177
  result.push(...groupRows)
467
- // 插入小计行
468
- const subtotalRow = this.createSubtotalRow(groupRows, groupKey)
178
+ const subtotalRow = this.createSubtotalRow(groupRows)
469
179
  result.push(subtotalRow)
470
180
  })
471
181
  } else {
472
182
  result = data
473
183
  }
474
184
 
475
- // 插入总计行
476
185
  if (hasTotal) {
477
186
  const totalRow = this.createTotalRow(data)
478
187
  result.push(totalRow)
479
188
  }
480
-
481
189
  return result
482
190
  },
483
191
 
484
- // 虚拟滚动总高度
485
- virtualTotalHeight() {
486
- const data = this.processedTableData
487
- return data.length * this.rowHeight
488
- },
489
-
490
- // 虚拟滚动偏移量
491
- virtualOffsetY() {
492
- const start = Math.max(0, this.visibleStartIndex - this.bufferSize)
493
- return start * this.rowHeight
494
- },
495
-
496
- // 扁平化列配置(处理 children 嵌套)
192
+ // 扁平化列配置,添加唯一 key
497
193
  flattenedColumns() {
498
194
  if (this.flattenedColumnsCache.length) return this.flattenedColumnsCache
499
- const result = []
500
- const flatten = (cols) => {
501
- cols.forEach(col => {
502
- const key = col.prop || col.label || Math.random().toString(36).slice(2)
503
- result.push({ ...col, _key: key })
195
+ const assignKeys = (cols, parentPath = '') => {
196
+ return cols.map((col, index) => {
197
+ const path = parentPath ? `${parentPath}_${index}` : String(index)
198
+ const key = col.prop || col.slot || col.label || path
199
+ const item = { ...col, _key: key }
200
+ if (col.children && col.children.length) {
201
+ item.children = assignKeys(col.children, path)
202
+ }
203
+ return item
504
204
  })
505
205
  }
506
- flatten(this.columns)
507
- this.flattenedColumnsCache = result
508
- return result
509
- },
510
-
511
- // 虚拟滚动时的可见数据
512
- visibleData() {
513
- const data = this.processedTableData
514
- if (!this.virtualScroll || data.length === 0) {
515
- return data
516
- }
517
- const start = Math.max(0, this.visibleStartIndex - this.bufferSize)
518
- const end = Math.min(data.length, this.visibleEndIndex + this.bufferSize)
519
- return data.slice(start, end)
206
+ this.flattenedColumnsCache = assignKeys(this.columns)
207
+ return this.flattenedColumnsCache
520
208
  }
521
209
  },
522
210
 
523
211
  watch: {
524
- tableData: {
525
- handler() {
526
- this.spanCache = {}
527
- this.flattenedColumnsCache = []
528
- if (this.virtualScroll) {
529
- this.$nextTick(() => {
530
- this.measureRowHeight()
531
- this.updateVisibleRange()
532
- })
533
- }
534
- },
535
- immediate: false
212
+ tableData() {
213
+ this.spanCache = {}
214
+ this.flattenedColumnsCache = []
536
215
  },
537
-
538
216
  columns: {
539
217
  handler() {
540
218
  this.flattenedColumnsCache = []
219
+ this.spanCache = {}
541
220
  },
542
221
  deep: true
543
222
  },
544
-
545
- virtualScroll(val) {
546
- if (val) {
547
- this.$nextTick(() => this.initVirtualScroll())
548
- } else {
549
- this.destroyVirtualScroll()
550
- }
223
+ groupColumns: {
224
+ handler() {
225
+ this.spanCache = {}
226
+ },
227
+ deep: true
551
228
  }
552
229
  },
553
230
 
554
231
  mounted() {
555
- if (this.virtualScroll) {
556
- this.$nextTick(() => this.initVirtualScroll())
557
- }
558
- // 监听窗口resize,更新表格宽度
559
- this.resizeHandler = () => {
560
- if (this.virtualScroll) {
561
- this.$nextTick(() => this.updateTableWidth())
562
- }
563
- }
564
- window.addEventListener('resize', this.resizeHandler)
232
+ this.$nextTick(() => {
233
+ window.addEventListener('resize', this.handleResize)
234
+ })
565
235
  },
566
236
 
567
237
  beforeDestroy() {
568
- this.destroyVirtualScroll()
569
- window.removeEventListener('resize', this.resizeHandler)
238
+ window.removeEventListener('resize', this.handleResize)
570
239
  },
571
240
 
572
241
  methods: {
573
- // ==================== 小计/总计工具函数 ====================
574
- // 找到第一个数据列(非 selection/index),用于显示"小计"/"总计"标签
242
+ handleResize() {
243
+ this.$nextTick(() => {
244
+ this.$refs.table && this.$refs.table.doLayout()
245
+ })
246
+ },
247
+
248
+ // ========== 小计 / 总计 相关 ==========
575
249
  findLabelColumn() {
576
250
  for (const col of this.columns) {
577
251
  if (col.children && col.children.length) {
@@ -585,33 +259,25 @@ export default {
585
259
  return { prop: '', label: '' }
586
260
  },
587
261
 
588
- // 按指定字段分组(保持分组顺序)
589
262
  groupData(data, groupBy) {
590
263
  const map = new Map()
591
264
  data.forEach(row => {
592
265
  const key = groupBy.map(f => row[f]).join('|||')
593
- if (!map.has(key)) {
594
- map.set(key, [])
595
- }
266
+ if (!map.has(key)) map.set(key, [])
596
267
  map.get(key).push(row)
597
268
  })
598
269
  return map
599
270
  },
600
271
 
601
- // 计算单列的聚合值
602
272
  _calcValue(rows, calc) {
603
- // 自定义函数
604
273
  if (typeof calc === 'function') return calc(rows)
605
-
606
- // 字符串:'sum' | 'avg' | 'count' | 'min' | 'max'(需配合 prop 使用)
607
-
608
- // 对象:{ prop: 'field', type: 'sum' } 或 { prop: 'field' } (默认 sum)
609
274
  const prop = calc.prop
610
275
  const type = calc.type || 'sum'
611
276
  const vals = prop ? rows.map(r => parseFloat(r[prop]) || 0) : []
612
277
  switch (type) {
613
278
  case 'sum': return vals.reduce((s, v) => s + v, 0)
614
- 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
615
281
  case 'count': return rows.length
616
282
  case 'min': return vals.length ? Math.min(...vals) : 0
617
283
  case 'max': return vals.length ? Math.max(...vals) : 0
@@ -619,20 +285,16 @@ export default {
619
285
  }
620
286
  },
621
287
 
622
- // 创建小计行
623
- createSubtotalRow(groupRows, groupKey) {
288
+ createSubtotalRow(groupRows) {
624
289
  const config = this.subtotalConfig
625
290
  const rawLabelProp = this.findLabelColumn()
626
-
627
- // 获取第一个 groupBy 列的值显示
628
291
  const labelField = config.groupBy ? config.groupBy[0] : ''
629
- const groupValue = groupRows[0] ? groupRows[0][labelField] : groupKey
292
+ const groupValue = groupRows[0] ? groupRows[0][labelField] : ''
630
293
  const labelText = config.labelText || `${groupValue} 小计`
631
294
 
632
- const row = { _rowType: 'subtotal', _groupKey: groupKey }
295
+ const row = { _rowType: 'subtotal' }
633
296
  row[rawLabelProp.prop] = labelText
634
297
 
635
- // 计算各列的聚合值
636
298
  if (config.columns) {
637
299
  Object.keys(config.columns).forEach(prop => {
638
300
  const calc = config.columns[prop]
@@ -641,11 +303,9 @@ export default {
641
303
  : this._calcValue(groupRows, calc)
642
304
  })
643
305
  }
644
-
645
306
  return row
646
307
  },
647
308
 
648
- // 创建总计行
649
309
  createTotalRow(allRows) {
650
310
  const config = this.totalConfig
651
311
  const rawLabelProp = this.findLabelColumn()
@@ -654,7 +314,6 @@ export default {
654
314
  const row = { _rowType: 'total' }
655
315
  row[rawLabelProp.prop] = labelText
656
316
 
657
- // 计算各列的聚合值
658
317
  if (config.columns) {
659
318
  Object.keys(config.columns).forEach(prop => {
660
319
  const calc = config.columns[prop]
@@ -663,55 +322,40 @@ export default {
663
322
  : this._calcValue(allRows, calc)
664
323
  })
665
324
  }
666
-
667
325
  return row
668
326
  },
669
327
 
670
- // 行类名(小计/总计行样式)
671
328
  getRowClassName({ row }) {
672
- if (!row) return ''
673
329
  if (row._rowType === 'subtotal') return 'ex-table-row-subtotal'
674
330
  if (row._rowType === 'total') return 'ex-table-row-total'
675
331
  return ''
676
332
  },
677
333
 
678
- // ==================== 列配置 ====================
334
+ // ========== 列处理 ==========
679
335
  getColumnProps(col) {
680
336
  const { _key, children, render, formatter, slot, ...props } = col
681
337
  return props
682
338
  },
683
339
 
684
- // ==================== 序号方法 ====================
685
340
  indexMethod(index) {
686
- if (this.virtualScroll && this.visibleData.length > 0) {
687
- const row = this.visibleData[index]
688
- if (row && row._realIndex !== undefined) {
689
- return row._realIndex + 1
690
- }
691
- }
692
341
  return index + 1
693
342
  },
694
343
 
695
- // ==================== 分组合并 ====================
344
+ // ========== 合并单元格 ==========
696
345
  handleSpanMethod({ row, column, rowIndex }) {
697
- // 跳过小计/总计行,不做合并
698
346
  if (row._rowType === 'subtotal' || row._rowType === 'total') {
699
347
  return { rowspan: 1, colspan: 1 }
700
348
  }
701
-
702
349
  if (!this.groupColumns.length) return { rowspan: 1, colspan: 1 }
703
350
 
704
- const data = this.virtualScroll ? this.visibleData : this.processedTableData
351
+ const data = this.processedTableData
705
352
  const prop = column.property
706
353
  const groupIndex = this.groupColumns.indexOf(prop)
707
-
708
354
  if (groupIndex === -1) return { rowspan: 1, colspan: 1 }
709
355
 
710
- // 检查缓存
711
356
  const cacheKey = `${rowIndex}_${prop}`
712
357
  if (this.spanCache[cacheKey]) return this.spanCache[cacheKey]
713
358
 
714
- // 判断当前行是否与前一行在分组列上完全相同
715
359
  if (rowIndex > 0) {
716
360
  const prevRow = data[rowIndex - 1]
717
361
  let isSame = true
@@ -723,190 +367,76 @@ export default {
723
367
  }
724
368
  }
725
369
  if (isSame) {
726
- const result = { rowspan: 0, colspan: 1 }
727
- this.spanCache[cacheKey] = result
728
- return result
370
+ const res = { rowspan: 0, colspan: 1 }
371
+ this.spanCache[cacheKey] = res
372
+ return res
729
373
  }
730
374
  }
731
375
 
732
- // 向下扫描计算合并行数
733
376
  let count = 1
734
377
  for (let i = rowIndex + 1; i < data.length; i++) {
735
378
  const nextRow = data[i]
736
- let isSame = true
379
+ let same = true
737
380
  for (let j = 0; j <= groupIndex; j++) {
738
381
  const gp = this.groupColumns[j]
739
382
  if (nextRow[gp] !== row[gp]) {
740
- isSame = false
383
+ same = false
741
384
  break
742
385
  }
743
386
  }
744
- if (isSame) {
745
- count++
746
- } else {
747
- break
748
- }
387
+ if (same) count++
388
+ else break
749
389
  }
750
390
 
751
- const result = { rowspan: count > 1 ? count : 1, colspan: 1 }
752
- this.spanCache[cacheKey] = result
753
- return result
391
+ const res = { rowspan: count > 1 ? count : 1, colspan: 1 }
392
+ this.spanCache[cacheKey] = res
393
+ return res
754
394
  },
755
395
 
756
- // ==================== 选择 ====================
396
+ // ========== 选择、排序、分页 ==========
757
397
  handleSelectionChange(rows) {
758
- // 过滤掉小计/总计行
759
398
  this.selectedRows = rows.filter(r => !r._rowType)
760
399
  this.$emit('selection-change', this.selectedRows)
761
400
  },
762
-
763
- // ==================== 排序 ====================
764
- handleSortChange({ prop, order }) {
765
- this.$emit('sort-change', { prop, order })
401
+ handleSortChange(info) {
402
+ this.$emit('sort-change', info)
766
403
  },
767
-
768
- // ==================== 分页 ====================
769
404
  handleSizeChange(size) {
770
405
  this.$emit('size-change', size)
771
406
  },
772
-
773
407
  handleCurrentChange(page) {
774
408
  this.$emit('page-change', page)
775
409
  },
776
410
 
777
- // ==================== 公开方法 ====================
411
+ // ========== 对外暴露方法 ==========
778
412
  getSelection() {
779
413
  return this.selectedRows
780
414
  },
781
-
782
415
  clearSelection() {
783
416
  this.$refs.table && this.$refs.table.clearSelection()
784
417
  this.selectedRows = []
785
418
  },
786
-
787
419
  toggleRowSelection(row, selected) {
788
420
  this.$refs.table && this.$refs.table.toggleRowSelection(row, selected)
789
421
  },
790
-
791
422
  toggleRowsSelection(rows, selected) {
792
- if (rows && rows.length) {
793
- rows.forEach(row => {
794
- this.$refs.table && this.$refs.table.toggleRowSelection(row, selected)
795
- })
796
- }
423
+ if (!rows || !rows.length) return
424
+ rows.forEach(row => {
425
+ this.$refs.table && this.$refs.table.toggleRowSelection(row, selected)
426
+ })
797
427
  },
798
-
799
428
  doLayout() {
800
429
  this.$refs.table && this.$refs.table.doLayout()
801
- },
802
-
803
- // 获取列宽度(支持自适应)
804
- getColumnWidth(col) {
805
- // 如果有固定宽度,使用固定宽度
806
- if (col.width) {
807
- return typeof col.width === 'number' ? col.width : col.width.replace('px', '')
808
- }
809
- // 非固定列且虚拟滚动时使用自适应宽度
810
- if (!col.fixed && this.virtualScroll) {
811
- return this.autoColumnWidth
812
- }
813
- // 默认宽度
814
- if (col.minWidth) {
815
- return typeof col.minWidth === 'number' ? col.minWidth : col.minWidth.replace('px', '')
816
- }
817
- return 100
818
- },
819
-
820
- // 更新表格宽度(用于自适应列宽计算)
821
- updateTableWidth() {
822
- const tableWrapper = this.$refs.tableBody
823
- if (tableWrapper) {
824
- this.tableWidth = tableWrapper.offsetWidth || 1000
825
- }
826
- },
827
-
828
- // ==================== 虚拟滚动 ====================
829
- initVirtualScroll() {
830
- this.updateTableWidth()
831
- this.measureRowHeight()
832
- if (this.processedTableData.length > 0) {
833
- this.updateVisibleRange()
834
- }
835
- },
836
-
837
- measureRowHeight() {
838
- const el = this.$refs.table && this.$refs.table.$el
839
- if (!el) return
840
- const row = el.querySelector('.el-table__row')
841
- if (row) {
842
- this.rowHeight = row.offsetHeight || this.rowInitHeight
843
- }
844
- },
845
-
846
- handleBodyScroll(event) {
847
- const target = event.target
848
- this.scrollLeft = target.scrollLeft
849
- if (this.virtualScroll) {
850
- this.scrollTop = target.scrollTop
851
- this.updateVisibleRange()
852
- }
853
- },
854
-
855
- updateVisibleRange() {
856
- const totalCount = this.processedTableData.length
857
- if (totalCount === 0 || this.rowHeight === 0) return
858
-
859
- const visibleCount = Math.ceil((this.computedHeight || 400) / this.rowHeight)
860
-
861
- this.visibleStartIndex = Math.max(0, Math.floor(this.scrollTop / this.rowHeight))
862
- this.visibleEndIndex = Math.min(
863
- totalCount,
864
- this.visibleStartIndex + visibleCount
865
- )
866
- this.visibleCount = visibleCount
867
- },
868
-
869
- destroyVirtualScroll() {
870
- this.scrollTop = 0
871
- this.visibleStartIndex = 0
872
- this.visibleEndIndex = 0
873
430
  }
874
431
  }
875
432
  }
876
433
  </script>
877
-
878
434
  <style scoped>
879
435
  .ex-table-wrapper {
880
436
  width: 100%;
881
437
  }
882
438
 
883
- /* 小计行样式 */
884
- .ex-table-wrapper >>> .ex-table-row-subtotal,
885
- .ex-table >>> .ex-table-row-subtotal {
886
- background-color: #f5f7fa;
887
- font-weight: 600;
888
- color: #303133;
889
- }
890
-
891
- /* 总计行样式 */
892
- .ex-table-wrapper >>> .ex-table-row-total,
893
- .ex-table >>> .ex-table-row-total {
894
- background-color: #e8eaed;
895
- font-weight: 700;
896
- color: #303133;
897
- border-top: 2px solid #c0c4cc;
898
- }
899
-
900
- .ex-table-wrapper >>> .ex-table-row-subtotal:hover > td,
901
- .ex-table >>> .ex-table-row-subtotal:hover > td {
902
- background-color: #ebeef5 !important;
903
- }
904
-
905
- .ex-table-wrapper >>> .ex-table-row-total:hover > td,
906
- .ex-table >>> .ex-table-row-total:hover > td {
907
- background-color: #dcdfe6 !important;
908
- }
909
-
439
+ /* 表头栏 */
910
440
  .ex-table-header {
911
441
  display: flex;
912
442
  align-items: center;
@@ -914,7 +444,6 @@ export default {
914
444
  padding: 12px 0;
915
445
  margin-bottom: 8px;
916
446
  }
917
-
918
447
  .ex-table-title {
919
448
  font-size: 16px;
920
449
  font-weight: 600;
@@ -922,7 +451,6 @@ export default {
922
451
  position: relative;
923
452
  padding-left: 12px;
924
453
  }
925
-
926
454
  .ex-table-title::before {
927
455
  content: '';
928
456
  position: absolute;
@@ -934,7 +462,6 @@ export default {
934
462
  background: #409eff;
935
463
  border-radius: 2px;
936
464
  }
937
-
938
465
  .ex-table-toolbar {
939
466
  display: flex;
940
467
  align-items: center;
@@ -943,62 +470,98 @@ export default {
943
470
 
944
471
  .ex-table-body {
945
472
  position: relative;
473
+ z-index: 1;
946
474
  }
947
475
 
948
- /* 虚拟滚动容器 */
949
- .ex-table-virtual {
950
- overflow: auto;
951
- position: relative;
476
+ /* 分页区域 */
477
+ .ex-table-footer {
478
+ display: flex;
479
+ justify-content: flex-end;
480
+ padding: 16px 0;
952
481
  }
953
482
 
954
- .ex-table-virtual-inner {
955
- position: relative;
956
- width: 100%;
483
+ /* ========== 小计/合计行样式 ========== */
484
+ ::v-deep .ex-table-row-subtotal {
485
+ background-color: #f5f7fa;
486
+ font-weight: 600;
487
+ color: #303133;
957
488
  }
958
-
959
- .ex-table-virtual-inner > div {
960
- 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;
961
500
  }
962
501
 
963
- .ex-table-footer {
964
- display: flex;
965
- justify-content: flex-end;
966
- 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;
967
524
  }
968
525
 
969
- /* 虚拟滚动固定表头 */
970
- .ex-table-fixed-header {
971
- overflow: hidden;
972
- border-bottom: 1px solid #ebeef5;
973
- background: #fafafa;
526
+ /* 允许单元格内文字选中,但容器本身不可选中 */
527
+ ::v-deep .el-table__body-wrapper .el-table__cell {
528
+ user-select: text;
529
+ -webkit-user-select: text;
974
530
  }
975
531
 
976
- .ex-table-fixed-header-table {
977
- table-layout: fixed;
978
- width: 100%;
979
- border-collapse: collapse;
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);
980
540
  }
981
541
 
982
- .ex-table-fixed-header-table th {
983
- padding: 12px 8px;
984
- text-align: left;
985
- font-weight: 500;
986
- font-size: 14px;
987
- color: #606266;
988
- border-bottom: 1px solid #ebeef5;
989
- border-right: 1px solid #ebeef5;
990
- white-space: nowrap;
991
- overflow: hidden;
992
- text-overflow: ellipsis;
993
- background: #fafafa;
994
- box-sizing: border-box;
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;
995
554
  }
996
555
 
997
- .ex-table-fixed-header-table th:last-child {
998
- border-right: none;
556
+ ::v-deep .el-table__fixed-header-wrapper,
557
+ ::v-deep .el-table__fixed-right-header-wrapper {
558
+ z-index: 11 !important;
999
559
  }
1000
560
 
1001
- .ex-table-fixed-header-table th:first-child {
1002
- padding-left: 16px;
561
+ /* ========== 虚拟滚动容器:正常布局流撑开滚动条 ========== */
562
+ ::v-deep .vs-phantom {
563
+ position: relative;
564
+ box-sizing: border-box;
565
+ z-index: 1 !important;
1003
566
  }
1004
- </style>
567
+ </style>