xt-element-ui 1.1.7 → 1.1.9

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