vue2-client 1.20.76 → 1.20.77

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,953 +1,953 @@
1
- import T from 'ant-design-vue/es/table/Table'
2
- import get from 'lodash.get'
3
- import { mapState } from 'vuex'
4
- import { STABLE_VIRTUAL_SCROLL_MIN_ROWS } from '@vue2-client/components/STable/virtualScrollThreshold'
5
- import { executeStrFunctionByContext } from '@vue2-client/utils/runEvalFunction'
6
-
7
- export default {
8
- data() {
9
- return {
10
- clickedRowKey: null,
11
- hoveredRowKey: null, // 跟踪鼠标悬浮的行
12
- needTotalList: [],
13
-
14
- selectedRows: [],
15
- selectedRowKeys: [],
16
-
17
- localLoading: false,
18
- localDataSource: [],
19
- localPagination: Object.assign({}, this.pagination),
20
-
21
- sortField: undefined,
22
- sortOrder: undefined,
23
- pageNum: 1,
24
- pageSize: 10,
25
- pageSizeOptions: ['10', '20', '30', '40']
26
- }
27
- },
28
- props: Object.assign({}, T.props, {
29
- rowKey: {
30
- type: [String, Function],
31
- default: 'key'
32
- },
33
- data: {
34
- type: Function,
35
- required: true
36
- },
37
- setScrollYHeight: {
38
- type: Function,
39
- required: true
40
- },
41
- showSizeChanger: {
42
- type: Boolean,
43
- default: true
44
- },
45
- showSummary: {
46
- type: Boolean,
47
- default: false
48
- },
49
- selectRowMode: {
50
- type: String,
51
- default: 'default'
52
- },
53
- size: {
54
- type: String,
55
- default: 'default'
56
- },
57
- /**
58
- * alert: {
59
- * show: true,
60
- * clear: Function
61
- * }
62
- */
63
- alert: {
64
- type: [Object, Boolean],
65
- default: null
66
- },
67
- rowSelection: {
68
- type: Object,
69
- default: null
70
- },
71
- /** @Deprecated */
72
- showAlertInfo: {
73
- type: Boolean,
74
- default: false
75
- },
76
- showPagination: {
77
- type: [String, Boolean],
78
- default: 'auto'
79
- },
80
- // 是否隐藏翻页,如果隐藏,彻底不显示翻译
81
- hidePagination: {
82
- type: Boolean,
83
- default: false
84
- },
85
- // 是否显示表头已选择部分
86
- showSelected: {
87
- type: Boolean,
88
- default: true
89
- },
90
- /**
91
- * enable page URI mode
92
- *
93
- * e.g:
94
- * /users/1
95
- * /users/2
96
- * /users/3?queryParam=test
97
- * ...
98
- */
99
- pageURI: {
100
- type: Boolean,
101
- default: false
102
- },
103
- // 最大分页大小,如果设置则使用该值作为分页大小并隐藏分页组件
104
- pageMaxSize: {
105
- type: Number,
106
- default: null
107
- },
108
- // 是否使用自定义分页样式
109
- customPagination: {
110
- type: Boolean,
111
- default: false
112
- },
113
- // 行样式函数,用于控制每行的样式类型
114
- rowStyleFunction: {
115
- type: [Function, String],
116
- default: null
117
- },
118
- // 默认查询的当页行数
119
- defaultPageSize: {
120
- type: Number,
121
- default: 10
122
- },
123
- pageSizeArray: {
124
- type: Array,
125
- default: () => ['10', '20', '30', '40']
126
- },
127
- /**
128
- * 为 false 时不向底层 a-table 传递 expandedRowRender 插槽。
129
- * Vue 2 中带 slot 名的 template 即使 v-if 为 false,仍可能把 expandedRowRender 注册进 $scopedSlots,导致出现展开列。
130
- */
131
- expandedRowEnabled: {
132
- type: Boolean,
133
- default: true
134
- }
135
- }),
136
- computed: {
137
- ...mapState('setting', ['theme']), // 添加 theme 到计算属性
138
-
139
- // 计算点击行的背景色,使用 10% 透明度
140
- clickedRowColor() {
141
- const themeColor = this.$store.state.setting.theme.color
142
- return this.hexToRgba(themeColor || '#1890ff', 0.1)
143
- },
144
- /** 当前页数据行数(仅监听长度变更,避免对行对象做深度 watch) */
145
- localDataSourceLength () {
146
- return Array.isArray(this.localDataSource) ? this.localDataSource.length : 0
147
- }
148
- },
149
- watch: {
150
- 'localPagination.current'(val) {
151
- this.pageURI &&
152
- this.$router.push({
153
- ...this.$route,
154
- name: this.$route.name,
155
- params: Object.assign({}, this.$route.params, {
156
- pageNo: val
157
- })
158
- })
159
- // change pagination, reset total data
160
- this.needTotalList = this.initTotalList(this.columns)
161
- if (this.selectRowMode === 'default') {
162
- this.selectedRowKeys = []
163
- this.selectedRows = []
164
- }
165
- },
166
- pageNum(val) {
167
- Object.assign(this.localPagination, {
168
- current: val
169
- })
170
- },
171
- pageSize(val) {
172
- Object.assign(this.localPagination, {
173
- pageSize: val
174
- })
175
- },
176
- showSizeChanger(val) {
177
- Object.assign(this.localPagination, {
178
- showSizeChanger: val
179
- })
180
- },
181
- pageMaxSize: {
182
- handler(val) {
183
- if (val && !this.defaultPageSize) {
184
- this.pageSize = val
185
- // 更新 localPagination
186
- if (this.localPagination) {
187
- Object.assign(this.localPagination, {
188
- pageSize: val
189
- })
190
- }
191
- }
192
- },
193
- immediate: true
194
- },
195
- pageSizeArray: {
196
- handler(val) {
197
- if (val && val.length) {
198
- this.pageSizeOptions = [...val]
199
- }
200
- },
201
- immediate: true
202
- },
203
- /** 同步当前数据源行数供外层(如 XTableWrapper)与纵向虚拟滚动行为对齐 */
204
- localDataSourceLength: {
205
- handler (len) {
206
- this.$emit('localDataSourceLengthChange', len)
207
- },
208
- immediate: true
209
- }
210
- },
211
- created() {
212
- const { pageNo } = this.$route.params
213
- const localPageNum = (this.pageURI && pageNo && parseInt(pageNo)) || this.pageNum
214
-
215
- // 优先使用 defaultPageSize,其次才使用 pageMaxSize(向后兼容)
216
- if (this.defaultPageSize) {
217
- this.pageSize = this.defaultPageSize
218
- }
219
- // 即将删除的过期属性
220
- if (this.pageMaxSize) {
221
- this.pageSize = this.pageMaxSize
222
- }
223
- // 新增属性-分页选项
224
- if (this.pageSizeArray && this.pageSizeArray.length) {
225
- this.pageSizeOptions = this.pageSizeArray
226
- }
227
-
228
- this.localPagination =
229
- (['auto', true].includes(this.showPagination) &&
230
- Object.assign({}, this.localPagination, {
231
- current: localPageNum,
232
- pageSize: this.pageSize,
233
- showSizeChanger: this.showSizeChanger
234
- })) ||
235
- false
236
- this.needTotalList = this.initTotalList(this.columns)
237
- // this.loadData()
238
- },
239
- methods: {
240
- /**
241
- * 为虚拟滚动写入稳定行键(与 ant-design-vue rowKey 语义对齐)
242
- */
243
- ensureVirtualKeysForRows(rows) {
244
- if (!rows || !rows.length) return
245
- const rk = this.rowKey
246
- rows.forEach((row, index) => {
247
- let keyVal
248
- if (typeof rk === 'function') {
249
- keyVal = rk(row, index)
250
- } else if (rk === '序号') {
251
- keyVal = index
252
- } else {
253
- keyVal = row[rk]
254
- }
255
- const k = keyVal != null && keyVal !== '' ? String(keyVal) : `__row_${index}`
256
- this.$set(row, '__v_key', k)
257
- })
258
- },
259
- /**
260
- * 父组件 setScrollYHeight 回调后,同步刷新虚拟表测量(表格高度/滚动容器变化)
261
- */
262
- invokeSetScrollYHeight(opts) {
263
- if (typeof this.setScrollYHeight === 'function') {
264
- this.setScrollYHeight(opts)
265
- }
266
- this.$nextTick(() => {
267
- const vt = this.$refs.vtable
268
- if (vt && typeof vt.update === 'function') {
269
- vt.update()
270
- }
271
- })
272
- },
273
- /**
274
- * 表格重新加载方法
275
- * 如果参数为 true, 则强制刷新到第一页
276
- * @param Boolean bool
277
- */
278
- refresh(bool = false) {
279
- bool &&
280
- (this.localPagination = Object.assign(
281
- {},
282
- {
283
- current: 1,
284
- pageSize: this.pageSize
285
- }
286
- ))
287
- this.loadData()
288
- },
289
- /**
290
- * 加载数据方法
291
- * @param {Object} pagination 分页选项器
292
- * @param {Object} filters 过滤条件
293
- * @param {Object} sorter 排序条件
294
- */
295
- loadData(pagination, filters, sorter) {
296
- this.localLoading = true
297
- // 暂存排序方式,避免 refresh 之后排序失效
298
- if (sorter && sorter.field) {
299
- this.sortField = sorter.field
300
- }
301
- if (sorter && sorter.order) {
302
- this.sortOrder = sorter.order
303
- }
304
-
305
- // 确定 pageSize
306
- let finalPageSize = this.pageSize
307
- if (this.showPagination === false) {
308
- // 当 showPagination 为 false 时,优先使用传入的 pageSize
309
- finalPageSize = this.defaultPageSize || pagination?.pageSize
310
- } else if (pagination && pagination.pageSize) {
311
- finalPageSize = pagination.pageSize
312
- } else if (this.localPagination && this.localPagination.pageSize) {
313
- finalPageSize = this.localPagination.pageSize
314
- }
315
-
316
- const parameter = Object.assign(
317
- {
318
- querySummary: !pagination && this.showSummary, // 分页查询的情况不重新获取汇总数据
319
- pageNo:
320
- (pagination && pagination.current) || (this.showPagination && this.localPagination.current) || this.pageNum,
321
- pageSize: finalPageSize
322
- },
323
- (this.sortField && { sortField: this.sortField }) || {},
324
- (this.sortOrder && { sortOrder: this.sortOrder }) || {},
325
- { ...filters }
326
- )
327
-
328
- // 在加载新数据前,将当前数据传递给父组件
329
- if (this.localDataSource && this.localDataSource.length > 0) {
330
- // 变化前的数据: oldData, 分页信息: pagination, 过滤条件: filters, 排序信息: sorter
331
- this.$emit('beforeDataChange', {
332
- oldData: this.localDataSource,
333
- pagination: this.localPagination,
334
- filters: filters,
335
- sorter: sorter
336
- })
337
- }
338
-
339
- const result = this.data(parameter)
340
- // 对接自己的通用数据接口需要修改下方代码中的 r.pageNo, r.totalCount, r.data
341
- // eslint-disable-next-line
342
- if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
343
- result.then(
344
- r => {
345
- this.localPagination =
346
- (this.showPagination &&
347
- Object.assign({}, this.localPagination, {
348
- current: r.pageNo, // 返回结果中的当前分页数
349
- total: r.totalCount, // 返回结果中的总记录数
350
- showSizeChanger: this.showSizeChanger,
351
- pageSize: finalPageSize
352
- })) ||
353
- false
354
- // 为防止删除数据后导致页面当前页面数据长度为 0 ,自动翻页到上一页
355
- if (r.data.length === 0 && this.showPagination && this.localPagination.current > 1) {
356
- this.localPagination.current--
357
- this.loadData()
358
- return
359
- }
360
-
361
- // 这里用于判断接口是否有返回 r.totalCount 且 this.showPagination = 'auto' 且 pageNo 和 pageSize 存在 且 totalCount 小于等于 pageNo * pageSize 的大小
362
- // 当情况满足时,表示数据不满足分页大小,关闭 table 分页功能
363
- try {
364
- if (['auto'].includes(this.showPagination) && r.totalCount <= r.pageNo * this.localPagination.pageSize) {
365
- this.localPagination.hideOnSinglePage = true
366
- }
367
- } catch (e) {
368
- this.localPagination = false
369
- }
370
- this.ensureVirtualKeysForRows(r.data)
371
- this.localDataSource = r.data // 返回结果中的数组数据
372
- this.localLoading = false
373
- // 清除选中行状态(翻页或重新查询时)
374
- this.clickedRowKey = null
375
- this.invokeSetScrollYHeight({ type: 'default' })
376
- },
377
- () => {
378
- this.localLoading = false
379
- this.invokeSetScrollYHeight({ type: 'default' })
380
- }
381
- )
382
- }
383
- },
384
- setLocalDataSource(data) {
385
- this.ensureVirtualKeysForRows(data)
386
- this.localDataSource = data
387
- },
388
- initTotalList(columns) {
389
- const totalList = []
390
- columns &&
391
- columns instanceof Array &&
392
- columns.forEach(column => {
393
- if (column.needTotal) {
394
- totalList.push({
395
- ...column,
396
- total: 0
397
- })
398
- }
399
- })
400
- return totalList
401
- },
402
- /**
403
- * 用于更新已选中的列表数据 total 统计
404
- * @param selectedRowKeys
405
- * @param selectedRows
406
- */
407
- updateSelect(selectedRowKeys, selectedRows) {
408
- this.selectedRows = selectedRows
409
- this.selectedRowKeys = selectedRowKeys
410
- const list = this.needTotalList
411
- this.needTotalList = list.map(item => {
412
- return {
413
- ...item,
414
- total: selectedRows.reduce((sum, val) => {
415
- const total = sum + parseInt(get(val, item.dataIndex))
416
- return isNaN(total) ? 0 : total
417
- }, 0)
418
- }
419
- })
420
- },
421
- /**
422
- * 清空 table 已选中项
423
- */
424
- clearSelected() {
425
- if (this.rowSelection) {
426
- this.rowSelection.onChange([], [])
427
- this.updateSelect([], [])
428
- }
429
- },
430
- /**
431
- * 处理交给 table 使用者去处理 clear 事件时,内部选中统计同时调用
432
- * @param callback
433
- * @returns {*}
434
- */
435
- renderClear(callback) {
436
- if (this.selectedRowKeys.length <= 0) return null
437
- return (
438
- <a
439
- style="margin-left: 24px"
440
- onClick={() => {
441
- callback()
442
- this.clearSelected()
443
- }}
444
- >
445
- 清空
446
- </a>
447
- )
448
- },
449
- renderAlert() {
450
- // 绘制统计列数据
451
- const needTotalItems = this.needTotalList.map(item => {
452
- return (
453
- <span style="margin-right: 12px">
454
- {item.title}总计{' '}
455
- <a style="font-weight: 600">{!item.customRender ? item.total : item.customRender(item.total)}</a>
456
- </span>
457
- )
458
- })
459
-
460
- // 绘制 清空 按钮
461
- const clearItem =
462
- typeof this.alert.clear === 'boolean' && this.alert.clear
463
- ? this.renderClear(this.clearSelected)
464
- : typeof this.alert.clear === 'function'
465
- ? this.renderClear(this.alert.clear)
466
- : null
467
-
468
- // 绘制 alert 组件
469
- return (
470
- <a-alert showIcon={true} style={{ marginBottom: '8px', fontSize: '14px' }}>
471
- <template slot="message">
472
- <span style="margin-right: 12px;">
473
- 已选择: <a style="font-weight: 600">{this.selectedRows.length}</a>
474
- </span>
475
- {needTotalItems}
476
- {clearItem}
477
- </template>
478
- </a-alert>
479
- )
480
- },
481
- onExpand(expanded, record) {
482
- this.$emit('expand', expanded, record)
483
- },
484
- handleRowClick(record, index, event, options = {}) {
485
- const { forceSelect = false, emitRowClick = true } = options
486
- const isRowKeySequence = this.rowKey === '序号'
487
- const currentRowKey = isRowKeySequence ? index : record[this.rowKey]
488
- const isDoubleClick = event && event.detail > 1
489
- const isSameRow = this.clickedRowKey === currentRowKey
490
-
491
- // 如果点击的是编辑单元格或操作列内的元素,不触发行点击事件
492
- if (event && event.target) {
493
- const clickedElement = event.target.closest
494
- ? event.target.closest('.innerTable, .ant-dropdown, .x-table-action-dropdown')
495
- : null
496
- if (clickedElement) {
497
- return
498
- }
499
- }
500
-
501
- if (forceSelect || isDoubleClick) {
502
- this.clickedRowKey = currentRowKey
503
- emitRowClick && this.$emit('rowClick', record)
504
- return
505
- }
506
-
507
- // 如果点击的是已选中的行,则取消选中;否则选中该行
508
- if (isSameRow) {
509
- this.clickedRowKey = null
510
- emitRowClick && this.$emit('rowClick', null) // 传递 null 表示取消选中
511
- } else {
512
- this.clickedRowKey = currentRowKey
513
- emitRowClick && this.$emit('rowClick', record)
514
- }
515
- },
516
- handleRowDoubleClick(record, index, event) {
517
- this.handleRowClick(record, index, event, { forceSelect: true })
518
- this.$emit('rowDblClick', record)
519
- },
520
- /** 悬停:默认关闭(避免 hoveredRowKey 触发 STable 整表重绘,滚动更顺)。需行 hover 高亮时再恢复内部赋值与 emit */
521
- handleRowMouseEnter() {},
522
- handleRowMouseLeave() {},
523
- hexToRgba(hex, alpha = 1) {
524
- try {
525
- const r = parseInt(hex.slice(1, 3), 16)
526
- const g = parseInt(hex.slice(3, 5), 16)
527
- const b = parseInt(hex.slice(5, 7), 16)
528
- return `rgba(${r}, ${g}, ${b}, ${alpha})`
529
- } catch (e) {
530
- return '#e6f7ff'
531
- }
532
- },
533
- // 获取行样式类型
534
- getRowStyleType(record, index) {
535
- if (this.rowStyleFunction) {
536
- try {
537
- // 如果是函数对象,直接执行
538
- if (typeof this.rowStyleFunction === 'function') {
539
- return this.rowStyleFunction(record, index)
540
- }
541
- // 如果是字符串函数,使用executeStrFunctionByContext执行
542
- if (typeof this.rowStyleFunction === 'string') {
543
- return executeStrFunctionByContext(this, this.rowStyleFunction, [record, index])
544
- }
545
- } catch (e) {
546
- console.warn('Row style function error:', e)
547
- return 'info'
548
- }
549
- }
550
- return 'info'
551
- },
552
- // 根据行样式类型和是否激活获取背景色
553
- // isActive: 是否处于 hover 或 selected 状态
554
- getRowBgColor(styleType, isActive = false) {
555
- const colorMap = {
556
- success: {
557
- normal: 'rgba(82, 196, 26, 0.2)',
558
- active: 'rgba(82, 196, 26, 0.2)'
559
- },
560
- warning: {
561
- normal: 'rgba(250, 173, 20, 0.1)',
562
- active: 'rgba(250, 173, 20, 0.2)'
563
- },
564
- error: {
565
- normal: 'rgba(245, 34, 47, 0.1)',
566
- active: 'rgba(245, 34, 47, 0.2)'
567
- },
568
- danger: {
569
- normal: 'rgba(245, 34, 47, 0.1)',
570
- active: 'rgba(245, 34, 47, 0.2)'
571
- },
572
- magic: {
573
- normal: 'rgba(114, 46, 209, 0.1)',
574
- active: 'rgba(114, 46, 209, 0.2)'
575
- },
576
- warn: {
577
- normal: 'rgba(250, 173, 20, 0.1)',
578
- active: 'rgba(250, 173, 20, 0.2)'
579
- },
580
- info: {
581
- normal: '',
582
- active: 'rgba(24, 144, 255, 0.2)'
583
- }
584
- }
585
-
586
- const colors = colorMap[styleType] || colorMap.info
587
- return isActive ? colors.active : colors.normal
588
- },
589
- // 获取行样式颜色(根据当前行的 hover 和 selected 状态)
590
- getRowStyleClass(record, index) {
591
- const isRowKeySequence = this.rowKey === '序号'
592
- const currentRowKey = isRowKeySequence ? index : record[this.rowKey]
593
-
594
- // 判断是否是选中行或悬浮行
595
- const isActive = this.clickedRowKey === currentRowKey || this.hoveredRowKey === currentRowKey
596
-
597
- // 获取行样式类型
598
- const styleType = this.getRowStyleType(record, index)
599
-
600
- // 返回对应颜色
601
- return this.getRowBgColor(styleType, isActive)
602
- }
603
- },
604
-
605
- render() {
606
- const props = {}
607
- const localKeys = Object.keys(this.$data)
608
- const showAlert =
609
- (typeof this.alert === 'object' &&
610
- this.alert !== null &&
611
- this.alert.show &&
612
- typeof this.rowSelection.selectedRowKeys !== 'undefined') ||
613
- this.alert
614
-
615
- Object.keys(T.props).forEach(k => {
616
- const localKey = `local${k.substring(0, 1).toUpperCase()}${k.substring(1)}`
617
- if (localKeys.includes(localKey)) {
618
- props[k] = this[localKey]
619
- return props[k]
620
- }
621
- if (k === 'rowSelection') {
622
- if (showAlert && this.rowSelection) {
623
- // 如果需要使用alert,则重新绑定 rowSelection 事件
624
- props[k] = {
625
- ...this.rowSelection,
626
- selectedRows: this.selectedRows,
627
- selectedRowKeys: this.selectedRowKeys,
628
- onChange: (selectedRowKeys, selectedRows) => {
629
- this.updateSelect(selectedRowKeys, selectedRows)
630
- typeof this[k].onChange !== 'undefined' && this[k].onChange(selectedRowKeys, selectedRows)
631
- }
632
- }
633
- return props[k]
634
- } else if (!this.rowSelection) {
635
- // 如果没打算开启 rowSelection 则清空默认的选择项
636
- props[k] = null
637
- return props[k]
638
- }
639
- }
640
- this[k] && (props[k] = this[k])
641
- return props[k]
642
- })
643
- // 取消原有的分页组件 产品设计分页组件和汇总在一行 重新分页逻辑
644
- props.pagination = false
645
- const tableData = Array.isArray(props.dataSource) ? props.dataSource : []
646
- if (props.scroll && props.scroll.y && tableData.length > STABLE_VIRTUAL_SCROLL_MIN_ROWS) {
647
- props.scroll = {
648
- ...props.scroll,
649
- virtual: { itemHeight: 60, overscan: 8 }
650
- }
651
- }
652
-
653
- // 使用 rowClassName 实现响应式行样式
654
- props.rowClassName = (record, index) => {
655
- const isRowKeySequence = this.rowKey === '序号'
656
- const currentRowKey = isRowKeySequence ? index : record[this.rowKey]
657
-
658
- const classes = []
659
-
660
- // 判断是否是选中行(点击状态)
661
- if (this.clickedRowKey === currentRowKey) {
662
- classes.push('row-clicked')
663
- }
664
-
665
- // 添加行样式类型(用于不同的颜色主题)
666
- const styleType = this.getRowStyleType(record, index)
667
- classes.push(`row-style-${styleType}`)
668
-
669
- return classes.join(' ')
670
- }
671
-
672
- // 在 props 中添加自定义行事件
673
- props.customRow = (record, index) => {
674
- return {
675
- on: {
676
- click: event => this.handleRowClick(record, index, event),
677
- dblclick: event => this.handleRowDoubleClick(record, index, event),
678
- mouseenter: () => this.handleRowMouseEnter(record, index),
679
- mouseleave: () => this.handleRowMouseLeave(record, index)
680
- }
681
- }
682
- }
683
- // 自定义底部汇总插槽组件
684
- const pagination = !!this.showPagination && (
685
- <a-row type={'flex'} justify={'start'} style={{ marginTop: '8px' }}>
686
- <a-col
687
- flex="1"
688
- style={{
689
- alignItems: 'center',
690
- display: 'flex',
691
- justifyContent: 'start',
692
- boxSizing: 'border-box',
693
- paddingRight: '2rem'
694
- }}
695
- >
696
- {this.$slots.fixedfooter}
697
- </a-col>
698
- <a-col flex="0 0">
699
- {this.customPagination ? (
700
- <div class="custom-pagination-container">
701
- {/* 首页按钮 */}
702
- <div
703
- class={['custom-pagination-btn', { disabled: this.localPagination.current === 1 }]}
704
- onClick={() => {
705
- if (this.localPagination.current > 1) {
706
- this.pageSize = this.localPagination.pageSize
707
- this.pageNum = 1
708
- this.loadData({
709
- current: 1,
710
- pageSize: this.localPagination.pageSize
711
- })
712
- }
713
- }}
714
- >
715
- <span class="custom-pagination-icon icon-rotate-right">⌅</span>
716
- </div>
717
-
718
- {/* 上一页按钮 */}
719
- <div
720
- class={['custom-pagination-btn', { disabled: this.localPagination.current === 1 }]}
721
- onClick={() => {
722
- if (this.localPagination.current > 1) {
723
- this.pageSize = this.localPagination.pageSize
724
- this.pageNum = this.localPagination.current - 1
725
- this.loadData({
726
- current: this.localPagination.current - 1,
727
- pageSize: this.localPagination.pageSize
728
- })
729
- }
730
- }}
731
- >
732
- &lt;
733
- </div>
734
-
735
- {/* 当前页按钮 */}
736
- <div class="custom-pagination-btn active">{this.localPagination.current}</div>
737
-
738
- {/* 下一页按钮 */}
739
- <div
740
- class={[
741
- 'custom-pagination-btn',
742
- {
743
- disabled:
744
- this.localPagination.current ===
745
- (this.localPagination && this.localPagination.pageSize
746
- ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
747
- : 0) ||
748
- !(this.localPagination && this.localPagination.pageSize
749
- ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
750
- : 0)
751
- }
752
- ]}
753
- onClick={() => {
754
- const totalPages =
755
- this.localPagination && this.localPagination.pageSize
756
- ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
757
- : 0
758
- if (totalPages > 0 && this.localPagination.current < totalPages) {
759
- this.pageSize = this.localPagination.pageSize
760
- this.pageNum = this.localPagination.current + 1
761
- this.loadData({
762
- current: this.localPagination.current + 1,
763
- pageSize: this.localPagination.pageSize
764
- })
765
- }
766
- }}
767
- >
768
- &gt;
769
- </div>
770
-
771
- {/* 末页按钮 */}
772
- <div
773
- class={[
774
- 'custom-pagination-btn',
775
- {
776
- disabled:
777
- this.localPagination.current ===
778
- (this.localPagination && this.localPagination.pageSize
779
- ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
780
- : 0) ||
781
- !(this.localPagination && this.localPagination.pageSize
782
- ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
783
- : 0)
784
- }
785
- ]}
786
- onClick={() => {
787
- const totalPages =
788
- this.localPagination && this.localPagination.pageSize
789
- ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
790
- : 0
791
- if (totalPages > 0 && this.localPagination.current < totalPages) {
792
- this.pageSize = this.localPagination.pageSize
793
- this.pageNum = totalPages
794
- this.loadData({
795
- current: totalPages,
796
- pageSize: this.localPagination.pageSize
797
- })
798
- }
799
- }}
800
- >
801
- <span class="custom-pagination-icon icon-rotate-left">⌅</span>
802
- </div>
803
-
804
- {/* 分页信息 */}
805
- <div class="custom-pagination-info">
806
-
807
- {this.localPagination && this.localPagination.pageSize
808
- ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
809
- : 0}
810
- 页,{' '}
811
- {this.localPagination && typeof this.localPagination.total !== 'undefined'
812
- ? this.localPagination.total
813
- : 0}
814
-
815
- </div>
816
- </div>
817
- ) : (
818
- <a-pagination
819
- total={this.localPagination.total}
820
- pageSize={this.localPagination.pageSize}
821
- current={this.localPagination.current}
822
- pageSizeOptions={this.pageSizeOptions}
823
- onChange={(page, pageSize) => {
824
- this.pageSize = pageSize
825
- this.pageNum = page
826
- this.loadData({
827
- current: page,
828
- pageSize: pageSize
829
- })
830
- }}
831
- onShowSizeChange={(page, pageSize) => {
832
- this.pageSize = pageSize
833
- this.pageNum = page
834
- this.loadData({
835
- current: page,
836
- pageSize: pageSize
837
- })
838
- }}
839
- show-total={(total, range) =>
840
- range[0] === range[1] ? `${range[0]} | 共 ${total} 项` : `${range[0]}-${range[1]} | 共 ${total} 项`
841
- }
842
- showSizeChanger={this.localPagination.showSizeChanger}
843
- />
844
- )}
845
- </a-col>
846
- </a-row>
847
- )
848
- const forwardedScopedSlots = { ...(this.$scopedSlots || {}) }
849
- if (!this.expandedRowEnabled) {
850
- delete forwardedScopedSlots.expandedRowRender
851
- }
852
- const table = (
853
- <a-table
854
- {...{ props, scopedSlots: forwardedScopedSlots }}
855
- onChange={this.loadData}
856
- onExpand={(expanded, record) => this.onExpand(expanded, record)}
857
- >
858
- {Object.keys(this.$slots).map(name => (
859
- <template slot={name}>{this.$slots[name]}</template>
860
- ))}
861
- </a-table>
862
- )
863
- return (
864
- <div class="table-wrapper">
865
- {showAlert && this.showSelected && this.selectRowMode === 'default' ? this.renderAlert() : null}
866
- {table}
867
- {!this.hidePagination ? pagination : null}
868
- </div>
869
- )
870
- }
871
- }
872
-
873
- // 添加自定义分页样式
874
- const customPaginationStyles = `
875
- <style>
876
- .custom-pagination-container {
877
- display: flex;
878
- align-items: center;
879
- gap: 8px;
880
- }
881
-
882
- .custom-pagination-btn {
883
- width: 32px;
884
- height: 32px;
885
- border: 1px solid #d9d9d9;
886
- border-radius: 6px;
887
- background: #fafafa;
888
- color: #5D5C5C;
889
- font-size: 14px;
890
- font-weight: 500;
891
- display: flex;
892
- align-items: center;
893
- justify-content: center;
894
- cursor: pointer;
895
- transition: all 0.3s;
896
- user-select: none;
897
- }
898
-
899
- .custom-pagination-btn:hover {
900
- border-color: #0057FE;
901
- color: #0057FE;
902
- }
903
-
904
- .custom-pagination-btn.active {
905
- background: #0057FE;
906
- border-color: #0057FE;
907
- color: #fff;
908
- }
909
-
910
- .custom-pagination-btn.disabled {
911
- background: #f5f5f5;
912
- border-color: #d9d9d9;
913
- color: #bfbfbf;
914
- cursor: not-allowed;
915
- }
916
-
917
- .custom-pagination-btn.disabled:hover {
918
- border-color: #d9d9d9;
919
- color: #bfbfbf;
920
- }
921
-
922
- .custom-pagination-info {
923
- margin-left: 16px;
924
- color: #666;
925
- font-size: 14px;
926
- white-space: nowrap;
927
- }
928
-
929
- /* 自定义图标与旋转方向 */
930
- .custom-pagination-icon {
931
- display: inline-block;
932
- line-height: 1;
933
- transform-origin: center;
934
- }
935
- .icon-rotate-left {
936
- transform: rotate(90deg);
937
- }
938
- .icon-rotate-right {
939
- transform: rotate(-90deg);
940
- }
941
- </style>
942
- `
943
-
944
- // 将样式注入到页面
945
- if (typeof document !== 'undefined') {
946
- const styleId = 'custom-pagination-styles'
947
- if (!document.getElementById(styleId)) {
948
- const styleElement = document.createElement('style')
949
- styleElement.id = styleId
950
- styleElement.textContent = customPaginationStyles.replace('<style>', '').replace('</style>', '')
951
- document.head.appendChild(styleElement)
952
- }
953
- }
1
+ import T from 'ant-design-vue/es/table/Table'
2
+ import get from 'lodash.get'
3
+ import { mapState } from 'vuex'
4
+ import { STABLE_VIRTUAL_SCROLL_MIN_ROWS } from '@vue2-client/components/STable/virtualScrollThreshold'
5
+ import { executeStrFunctionByContext } from '@vue2-client/utils/runEvalFunction'
6
+
7
+ export default {
8
+ data() {
9
+ return {
10
+ clickedRowKey: null,
11
+ hoveredRowKey: null, // 跟踪鼠标悬浮的行
12
+ needTotalList: [],
13
+
14
+ selectedRows: [],
15
+ selectedRowKeys: [],
16
+
17
+ localLoading: false,
18
+ localDataSource: [],
19
+ localPagination: Object.assign({}, this.pagination),
20
+
21
+ sortField: undefined,
22
+ sortOrder: undefined,
23
+ pageNum: 1,
24
+ pageSize: 10,
25
+ pageSizeOptions: ['10', '20', '30', '40']
26
+ }
27
+ },
28
+ props: Object.assign({}, T.props, {
29
+ rowKey: {
30
+ type: [String, Function],
31
+ default: 'key'
32
+ },
33
+ data: {
34
+ type: Function,
35
+ required: true
36
+ },
37
+ setScrollYHeight: {
38
+ type: Function,
39
+ required: true
40
+ },
41
+ showSizeChanger: {
42
+ type: Boolean,
43
+ default: true
44
+ },
45
+ showSummary: {
46
+ type: Boolean,
47
+ default: false
48
+ },
49
+ selectRowMode: {
50
+ type: String,
51
+ default: 'default'
52
+ },
53
+ size: {
54
+ type: String,
55
+ default: 'default'
56
+ },
57
+ /**
58
+ * alert: {
59
+ * show: true,
60
+ * clear: Function
61
+ * }
62
+ */
63
+ alert: {
64
+ type: [Object, Boolean],
65
+ default: null
66
+ },
67
+ rowSelection: {
68
+ type: Object,
69
+ default: null
70
+ },
71
+ /** @Deprecated */
72
+ showAlertInfo: {
73
+ type: Boolean,
74
+ default: false
75
+ },
76
+ showPagination: {
77
+ type: [String, Boolean],
78
+ default: 'auto'
79
+ },
80
+ // 是否隐藏翻页,如果隐藏,彻底不显示翻译
81
+ hidePagination: {
82
+ type: Boolean,
83
+ default: false
84
+ },
85
+ // 是否显示表头已选择部分
86
+ showSelected: {
87
+ type: Boolean,
88
+ default: true
89
+ },
90
+ /**
91
+ * enable page URI mode
92
+ *
93
+ * e.g:
94
+ * /users/1
95
+ * /users/2
96
+ * /users/3?queryParam=test
97
+ * ...
98
+ */
99
+ pageURI: {
100
+ type: Boolean,
101
+ default: false
102
+ },
103
+ // 最大分页大小,如果设置则使用该值作为分页大小并隐藏分页组件
104
+ pageMaxSize: {
105
+ type: Number,
106
+ default: null
107
+ },
108
+ // 是否使用自定义分页样式
109
+ customPagination: {
110
+ type: Boolean,
111
+ default: false
112
+ },
113
+ // 行样式函数,用于控制每行的样式类型
114
+ rowStyleFunction: {
115
+ type: [Function, String],
116
+ default: null
117
+ },
118
+ // 默认查询的当页行数
119
+ defaultPageSize: {
120
+ type: Number,
121
+ default: 10
122
+ },
123
+ pageSizeArray: {
124
+ type: Array,
125
+ default: () => ['10', '20', '30', '40']
126
+ },
127
+ /**
128
+ * 为 false 时不向底层 a-table 传递 expandedRowRender 插槽。
129
+ * Vue 2 中带 slot 名的 template 即使 v-if 为 false,仍可能把 expandedRowRender 注册进 $scopedSlots,导致出现展开列。
130
+ */
131
+ expandedRowEnabled: {
132
+ type: Boolean,
133
+ default: true
134
+ }
135
+ }),
136
+ computed: {
137
+ ...mapState('setting', ['theme']), // 添加 theme 到计算属性
138
+
139
+ // 计算点击行的背景色,使用 10% 透明度
140
+ clickedRowColor() {
141
+ const themeColor = this.$store.state.setting.theme.color
142
+ return this.hexToRgba(themeColor || '#1890ff', 0.1)
143
+ },
144
+ /** 当前页数据行数(仅监听长度变更,避免对行对象做深度 watch) */
145
+ localDataSourceLength() {
146
+ return Array.isArray(this.localDataSource) ? this.localDataSource.length : 0
147
+ }
148
+ },
149
+ watch: {
150
+ 'localPagination.current'(val) {
151
+ this.pageURI &&
152
+ this.$router.push({
153
+ ...this.$route,
154
+ name: this.$route.name,
155
+ params: Object.assign({}, this.$route.params, {
156
+ pageNo: val
157
+ })
158
+ })
159
+ // change pagination, reset total data
160
+ this.needTotalList = this.initTotalList(this.columns)
161
+ if (this.selectRowMode === 'default') {
162
+ this.selectedRowKeys = []
163
+ this.selectedRows = []
164
+ }
165
+ },
166
+ pageNum(val) {
167
+ Object.assign(this.localPagination, {
168
+ current: val
169
+ })
170
+ },
171
+ pageSize(val) {
172
+ Object.assign(this.localPagination, {
173
+ pageSize: val
174
+ })
175
+ },
176
+ showSizeChanger(val) {
177
+ Object.assign(this.localPagination, {
178
+ showSizeChanger: val
179
+ })
180
+ },
181
+ pageMaxSize: {
182
+ handler(val) {
183
+ if (val && !this.defaultPageSize) {
184
+ this.pageSize = val
185
+ // 更新 localPagination
186
+ if (this.localPagination) {
187
+ Object.assign(this.localPagination, {
188
+ pageSize: val
189
+ })
190
+ }
191
+ }
192
+ },
193
+ immediate: true
194
+ },
195
+ pageSizeArray: {
196
+ handler(val) {
197
+ if (val && val.length) {
198
+ this.pageSizeOptions = [...val]
199
+ }
200
+ },
201
+ immediate: true
202
+ },
203
+ /** 同步当前数据源行数供外层(如 XTableWrapper)与纵向虚拟滚动行为对齐 */
204
+ localDataSourceLength: {
205
+ handler(len) {
206
+ this.$emit('localDataSourceLengthChange', len)
207
+ },
208
+ immediate: true
209
+ }
210
+ },
211
+ created() {
212
+ const { pageNo } = this.$route.params
213
+ const localPageNum = (this.pageURI && pageNo && parseInt(pageNo)) || this.pageNum
214
+
215
+ // 优先使用 defaultPageSize,其次才使用 pageMaxSize(向后兼容)
216
+ if (this.defaultPageSize) {
217
+ this.pageSize = this.defaultPageSize
218
+ }
219
+ // 即将删除的过期属性
220
+ if (this.pageMaxSize) {
221
+ this.pageSize = this.pageMaxSize
222
+ }
223
+ // 新增属性-分页选项
224
+ if (this.pageSizeArray && this.pageSizeArray.length) {
225
+ this.pageSizeOptions = this.pageSizeArray
226
+ }
227
+
228
+ this.localPagination =
229
+ (['auto', true].includes(this.showPagination) &&
230
+ Object.assign({}, this.localPagination, {
231
+ current: localPageNum,
232
+ pageSize: this.pageSize,
233
+ showSizeChanger: this.showSizeChanger
234
+ })) ||
235
+ false
236
+ this.needTotalList = this.initTotalList(this.columns)
237
+ // this.loadData()
238
+ },
239
+ methods: {
240
+ /**
241
+ * 为虚拟滚动写入稳定行键(与 ant-design-vue rowKey 语义对齐)
242
+ */
243
+ ensureVirtualKeysForRows(rows) {
244
+ if (!rows || !rows.length) return
245
+ const rk = this.rowKey
246
+ rows.forEach((row, index) => {
247
+ let keyVal
248
+ if (typeof rk === 'function') {
249
+ keyVal = rk(row, index)
250
+ } else if (rk === '序号') {
251
+ keyVal = index
252
+ } else {
253
+ keyVal = row[rk]
254
+ }
255
+ const k = keyVal != null && keyVal !== '' ? String(keyVal) : `__row_${index}`
256
+ this.$set(row, '__v_key', k)
257
+ })
258
+ },
259
+ /**
260
+ * 父组件 setScrollYHeight 回调后,同步刷新虚拟表测量(表格高度/滚动容器变化)
261
+ */
262
+ invokeSetScrollYHeight(opts) {
263
+ if (typeof this.setScrollYHeight === 'function') {
264
+ this.setScrollYHeight(opts)
265
+ }
266
+ this.$nextTick(() => {
267
+ const vt = this.$refs.vtable
268
+ if (vt && typeof vt.update === 'function') {
269
+ vt.update()
270
+ }
271
+ })
272
+ },
273
+ /**
274
+ * 表格重新加载方法
275
+ * 如果参数为 true, 则强制刷新到第一页
276
+ * @param Boolean bool
277
+ */
278
+ refresh(bool = false) {
279
+ bool &&
280
+ (this.localPagination = Object.assign(
281
+ {},
282
+ {
283
+ current: 1,
284
+ pageSize: this.pageSize
285
+ }
286
+ ))
287
+ this.loadData()
288
+ },
289
+ /**
290
+ * 加载数据方法
291
+ * @param {Object} pagination 分页选项器
292
+ * @param {Object} filters 过滤条件
293
+ * @param {Object} sorter 排序条件
294
+ */
295
+ loadData(pagination, filters, sorter) {
296
+ this.localLoading = true
297
+ // 暂存排序方式,避免 refresh 之后排序失效
298
+ if (sorter && sorter.field) {
299
+ this.sortField = sorter.field
300
+ }
301
+ if (sorter && sorter.order) {
302
+ this.sortOrder = sorter.order
303
+ }
304
+
305
+ // 确定 pageSize
306
+ let finalPageSize = this.pageSize
307
+ if (this.showPagination === false) {
308
+ // 当 showPagination 为 false 时,优先使用传入的 pageSize
309
+ finalPageSize = this.defaultPageSize || pagination?.pageSize
310
+ } else if (pagination && pagination.pageSize) {
311
+ finalPageSize = pagination.pageSize
312
+ } else if (this.localPagination && this.localPagination.pageSize) {
313
+ finalPageSize = this.localPagination.pageSize
314
+ }
315
+
316
+ const parameter = Object.assign(
317
+ {
318
+ querySummary: !pagination && this.showSummary, // 分页查询的情况不重新获取汇总数据
319
+ pageNo:
320
+ (pagination && pagination.current) || (this.showPagination && this.localPagination.current) || this.pageNum,
321
+ pageSize: finalPageSize
322
+ },
323
+ (this.sortField && { sortField: this.sortField }) || {},
324
+ (this.sortOrder && { sortOrder: this.sortOrder }) || {},
325
+ { ...filters }
326
+ )
327
+
328
+ // 在加载新数据前,将当前数据传递给父组件
329
+ if (this.localDataSource && this.localDataSource.length > 0) {
330
+ // 变化前的数据: oldData, 分页信息: pagination, 过滤条件: filters, 排序信息: sorter
331
+ this.$emit('beforeDataChange', {
332
+ oldData: this.localDataSource,
333
+ pagination: this.localPagination,
334
+ filters: filters,
335
+ sorter: sorter
336
+ })
337
+ }
338
+
339
+ const result = this.data(parameter)
340
+ // 对接自己的通用数据接口需要修改下方代码中的 r.pageNo, r.totalCount, r.data
341
+ // eslint-disable-next-line
342
+ if ((typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
343
+ result.then(
344
+ r => {
345
+ this.localPagination =
346
+ (this.showPagination &&
347
+ Object.assign({}, this.localPagination, {
348
+ current: r.pageNo, // 返回结果中的当前分页数
349
+ total: r.totalCount, // 返回结果中的总记录数
350
+ showSizeChanger: this.showSizeChanger,
351
+ pageSize: finalPageSize
352
+ })) ||
353
+ false
354
+ // 为防止删除数据后导致页面当前页面数据长度为 0 ,自动翻页到上一页
355
+ if (r.data.length === 0 && this.showPagination && this.localPagination.current > 1) {
356
+ this.localPagination.current--
357
+ this.loadData()
358
+ return
359
+ }
360
+
361
+ // 这里用于判断接口是否有返回 r.totalCount 且 this.showPagination = 'auto' 且 pageNo 和 pageSize 存在 且 totalCount 小于等于 pageNo * pageSize 的大小
362
+ // 当情况满足时,表示数据不满足分页大小,关闭 table 分页功能
363
+ try {
364
+ if (['auto'].includes(this.showPagination) && r.totalCount <= r.pageNo * this.localPagination.pageSize) {
365
+ this.localPagination.hideOnSinglePage = true
366
+ }
367
+ } catch (e) {
368
+ this.localPagination = false
369
+ }
370
+ this.ensureVirtualKeysForRows(r.data)
371
+ this.localDataSource = r.data // 返回结果中的数组数据
372
+ this.localLoading = false
373
+ // 清除选中行状态(翻页或重新查询时)
374
+ this.clickedRowKey = null
375
+ this.invokeSetScrollYHeight({ type: 'default' })
376
+ },
377
+ () => {
378
+ this.localLoading = false
379
+ this.invokeSetScrollYHeight({ type: 'default' })
380
+ }
381
+ )
382
+ }
383
+ },
384
+ setLocalDataSource(data) {
385
+ this.ensureVirtualKeysForRows(data)
386
+ this.localDataSource = data
387
+ },
388
+ initTotalList(columns) {
389
+ const totalList = []
390
+ columns &&
391
+ columns instanceof Array &&
392
+ columns.forEach(column => {
393
+ if (column.needTotal) {
394
+ totalList.push({
395
+ ...column,
396
+ total: 0
397
+ })
398
+ }
399
+ })
400
+ return totalList
401
+ },
402
+ /**
403
+ * 用于更新已选中的列表数据 total 统计
404
+ * @param selectedRowKeys
405
+ * @param selectedRows
406
+ */
407
+ updateSelect(selectedRowKeys, selectedRows) {
408
+ this.selectedRows = selectedRows
409
+ this.selectedRowKeys = selectedRowKeys
410
+ const list = this.needTotalList
411
+ this.needTotalList = list.map(item => {
412
+ return {
413
+ ...item,
414
+ total: selectedRows.reduce((sum, val) => {
415
+ const total = sum + parseInt(get(val, item.dataIndex))
416
+ return isNaN(total) ? 0 : total
417
+ }, 0)
418
+ }
419
+ })
420
+ },
421
+ /**
422
+ * 清空 table 已选中项
423
+ */
424
+ clearSelected() {
425
+ if (this.rowSelection) {
426
+ this.rowSelection.onChange([], [])
427
+ this.updateSelect([], [])
428
+ }
429
+ },
430
+ /**
431
+ * 处理交给 table 使用者去处理 clear 事件时,内部选中统计同时调用
432
+ * @param callback
433
+ * @returns {*}
434
+ */
435
+ renderClear(callback) {
436
+ if (this.selectedRowKeys.length <= 0) return null
437
+ return (
438
+ <a
439
+ style="margin-left: 24px"
440
+ onClick={() => {
441
+ callback()
442
+ this.clearSelected()
443
+ }}
444
+ >
445
+ 清空
446
+ </a>
447
+ )
448
+ },
449
+ renderAlert() {
450
+ // 绘制统计列数据
451
+ const needTotalItems = this.needTotalList.map(item => {
452
+ return (
453
+ <span style="margin-right: 12px">
454
+ {item.title}总计{' '}
455
+ <a style="font-weight: 600">{!item.customRender ? item.total : item.customRender(item.total)}</a>
456
+ </span>
457
+ )
458
+ })
459
+
460
+ // 绘制 清空 按钮
461
+ const clearItem =
462
+ typeof this.alert.clear === 'boolean' && this.alert.clear
463
+ ? this.renderClear(this.clearSelected)
464
+ : typeof this.alert.clear === 'function'
465
+ ? this.renderClear(this.alert.clear)
466
+ : null
467
+
468
+ // 绘制 alert 组件
469
+ return (
470
+ <a-alert showIcon={true} style={{ marginBottom: '8px', fontSize: '14px' }}>
471
+ <template slot="message">
472
+ <span style="margin-right: 12px;">
473
+ 已选择: <a style="font-weight: 600">{this.selectedRows.length}</a>
474
+ </span>
475
+ {needTotalItems}
476
+ {clearItem}
477
+ </template>
478
+ </a-alert>
479
+ )
480
+ },
481
+ onExpand(expanded, record) {
482
+ this.$emit('expand', expanded, record)
483
+ },
484
+ handleRowClick(record, index, event, options = {}) {
485
+ const { forceSelect = false, emitRowClick = true } = options
486
+ const isRowKeySequence = this.rowKey === '序号'
487
+ const currentRowKey = isRowKeySequence ? index : record[this.rowKey]
488
+ const isDoubleClick = event && event.detail > 1
489
+ const isSameRow = this.clickedRowKey === currentRowKey
490
+
491
+ // 如果点击的是编辑单元格或操作列内的元素,不触发行点击事件
492
+ if (event && event.target) {
493
+ const clickedElement = event.target.closest
494
+ ? event.target.closest('.innerTable, .ant-dropdown, .x-table-action-dropdown')
495
+ : null
496
+ if (clickedElement) {
497
+ return
498
+ }
499
+ }
500
+
501
+ if (forceSelect || isDoubleClick) {
502
+ this.clickedRowKey = currentRowKey
503
+ emitRowClick && this.$emit('rowClick', record)
504
+ return
505
+ }
506
+
507
+ // 如果点击的是已选中的行,则取消选中;否则选中该行
508
+ if (isSameRow) {
509
+ this.clickedRowKey = null
510
+ emitRowClick && this.$emit('rowClick', null) // 传递 null 表示取消选中
511
+ } else {
512
+ this.clickedRowKey = currentRowKey
513
+ emitRowClick && this.$emit('rowClick', record)
514
+ }
515
+ },
516
+ handleRowDoubleClick(record, index, event) {
517
+ this.handleRowClick(record, index, event, { forceSelect: true })
518
+ this.$emit('rowDblClick', record)
519
+ },
520
+ /** 悬停:默认关闭(避免 hoveredRowKey 触发 STable 整表重绘,滚动更顺)。需行 hover 高亮时再恢复内部赋值与 emit */
521
+ handleRowMouseEnter() {},
522
+ handleRowMouseLeave() {},
523
+ hexToRgba(hex, alpha = 1) {
524
+ try {
525
+ const r = parseInt(hex.slice(1, 3), 16)
526
+ const g = parseInt(hex.slice(3, 5), 16)
527
+ const b = parseInt(hex.slice(5, 7), 16)
528
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`
529
+ } catch (e) {
530
+ return '#e6f7ff'
531
+ }
532
+ },
533
+ // 获取行样式类型
534
+ getRowStyleType(record, index) {
535
+ if (this.rowStyleFunction) {
536
+ try {
537
+ // 如果是函数对象,直接执行
538
+ if (typeof this.rowStyleFunction === 'function') {
539
+ return this.rowStyleFunction(record, index)
540
+ }
541
+ // 如果是字符串函数,使用executeStrFunctionByContext执行
542
+ if (typeof this.rowStyleFunction === 'string') {
543
+ return executeStrFunctionByContext(this, this.rowStyleFunction, [record, index])
544
+ }
545
+ } catch (e) {
546
+ console.warn('Row style function error:', e)
547
+ return 'info'
548
+ }
549
+ }
550
+ return 'info'
551
+ },
552
+ // 根据行样式类型和是否激活获取背景色
553
+ // isActive: 是否处于 hover 或 selected 状态
554
+ getRowBgColor(styleType, isActive = false) {
555
+ const colorMap = {
556
+ success: {
557
+ normal: 'rgba(82, 196, 26, 0.2)',
558
+ active: 'rgba(82, 196, 26, 0.2)'
559
+ },
560
+ warning: {
561
+ normal: 'rgba(250, 173, 20, 0.1)',
562
+ active: 'rgba(250, 173, 20, 0.2)'
563
+ },
564
+ error: {
565
+ normal: 'rgba(245, 34, 47, 0.1)',
566
+ active: 'rgba(245, 34, 47, 0.2)'
567
+ },
568
+ danger: {
569
+ normal: 'rgba(245, 34, 47, 0.1)',
570
+ active: 'rgba(245, 34, 47, 0.2)'
571
+ },
572
+ magic: {
573
+ normal: 'rgba(114, 46, 209, 0.1)',
574
+ active: 'rgba(114, 46, 209, 0.2)'
575
+ },
576
+ warn: {
577
+ normal: 'rgba(250, 173, 20, 0.1)',
578
+ active: 'rgba(250, 173, 20, 0.2)'
579
+ },
580
+ info: {
581
+ normal: '',
582
+ active: 'rgba(24, 144, 255, 0.2)'
583
+ }
584
+ }
585
+
586
+ const colors = colorMap[styleType] || colorMap.info
587
+ return isActive ? colors.active : colors.normal
588
+ },
589
+ // 获取行样式颜色(根据当前行的 hover 和 selected 状态)
590
+ getRowStyleClass(record, index) {
591
+ const isRowKeySequence = this.rowKey === '序号'
592
+ const currentRowKey = isRowKeySequence ? index : record[this.rowKey]
593
+
594
+ // 判断是否是选中行或悬浮行
595
+ const isActive = this.clickedRowKey === currentRowKey || this.hoveredRowKey === currentRowKey
596
+
597
+ // 获取行样式类型
598
+ const styleType = this.getRowStyleType(record, index)
599
+
600
+ // 返回对应颜色
601
+ return this.getRowBgColor(styleType, isActive)
602
+ }
603
+ },
604
+
605
+ render() {
606
+ const props = {}
607
+ const localKeys = Object.keys(this.$data)
608
+ const showAlert =
609
+ (typeof this.alert === 'object' &&
610
+ this.alert !== null &&
611
+ this.alert.show &&
612
+ typeof this.rowSelection.selectedRowKeys !== 'undefined') ||
613
+ this.alert
614
+
615
+ Object.keys(T.props).forEach(k => {
616
+ const localKey = `local${k.substring(0, 1).toUpperCase()}${k.substring(1)}`
617
+ if (localKeys.includes(localKey)) {
618
+ props[k] = this[localKey]
619
+ return props[k]
620
+ }
621
+ if (k === 'rowSelection') {
622
+ if (showAlert && this.rowSelection) {
623
+ // 如果需要使用alert,则重新绑定 rowSelection 事件
624
+ props[k] = {
625
+ ...this.rowSelection,
626
+ selectedRows: this.selectedRows,
627
+ selectedRowKeys: this.selectedRowKeys,
628
+ onChange: (selectedRowKeys, selectedRows) => {
629
+ this.updateSelect(selectedRowKeys, selectedRows)
630
+ typeof this[k].onChange !== 'undefined' && this[k].onChange(selectedRowKeys, selectedRows)
631
+ }
632
+ }
633
+ return props[k]
634
+ } else if (!this.rowSelection) {
635
+ // 如果没打算开启 rowSelection 则清空默认的选择项
636
+ props[k] = null
637
+ return props[k]
638
+ }
639
+ }
640
+ this[k] && (props[k] = this[k])
641
+ return props[k]
642
+ })
643
+ // 取消原有的分页组件 产品设计分页组件和汇总在一行 重新分页逻辑
644
+ props.pagination = false
645
+ const tableData = Array.isArray(props.dataSource) ? props.dataSource : []
646
+ if (props.scroll && props.scroll.y && tableData.length > STABLE_VIRTUAL_SCROLL_MIN_ROWS) {
647
+ props.scroll = {
648
+ ...props.scroll,
649
+ virtual: { itemHeight: 60, overscan: 10 }
650
+ }
651
+ }
652
+
653
+ // 使用 rowClassName 实现响应式行样式
654
+ props.rowClassName = (record, index) => {
655
+ const isRowKeySequence = this.rowKey === '序号'
656
+ const currentRowKey = isRowKeySequence ? index : record[this.rowKey]
657
+
658
+ const classes = []
659
+
660
+ // 判断是否是选中行(点击状态)
661
+ if (this.clickedRowKey === currentRowKey) {
662
+ classes.push('row-clicked')
663
+ }
664
+
665
+ // 添加行样式类型(用于不同的颜色主题)
666
+ const styleType = this.getRowStyleType(record, index)
667
+ classes.push(`row-style-${styleType}`)
668
+
669
+ return classes.join(' ')
670
+ }
671
+
672
+ // 在 props 中添加自定义行事件
673
+ props.customRow = (record, index) => {
674
+ return {
675
+ on: {
676
+ click: event => this.handleRowClick(record, index, event),
677
+ dblclick: event => this.handleRowDoubleClick(record, index, event),
678
+ mouseenter: () => this.handleRowMouseEnter(record, index),
679
+ mouseleave: () => this.handleRowMouseLeave(record, index)
680
+ }
681
+ }
682
+ }
683
+ // 自定义底部汇总插槽组件
684
+ const pagination = !!this.showPagination && (
685
+ <a-row type={'flex'} justify={'start'} style={{ marginTop: '8px' }}>
686
+ <a-col
687
+ flex="1"
688
+ style={{
689
+ alignItems: 'center',
690
+ display: 'flex',
691
+ justifyContent: 'start',
692
+ boxSizing: 'border-box',
693
+ paddingRight: '2rem'
694
+ }}
695
+ >
696
+ {this.$slots.fixedfooter}
697
+ </a-col>
698
+ <a-col flex="0 0">
699
+ {this.customPagination ? (
700
+ <div class="custom-pagination-container">
701
+ {/* 首页按钮 */}
702
+ <div
703
+ class={['custom-pagination-btn', { disabled: this.localPagination.current === 1 }]}
704
+ onClick={() => {
705
+ if (this.localPagination.current > 1) {
706
+ this.pageSize = this.localPagination.pageSize
707
+ this.pageNum = 1
708
+ this.loadData({
709
+ current: 1,
710
+ pageSize: this.localPagination.pageSize
711
+ })
712
+ }
713
+ }}
714
+ >
715
+ <span class="custom-pagination-icon icon-rotate-right">⌅</span>
716
+ </div>
717
+
718
+ {/* 上一页按钮 */}
719
+ <div
720
+ class={['custom-pagination-btn', { disabled: this.localPagination.current === 1 }]}
721
+ onClick={() => {
722
+ if (this.localPagination.current > 1) {
723
+ this.pageSize = this.localPagination.pageSize
724
+ this.pageNum = this.localPagination.current - 1
725
+ this.loadData({
726
+ current: this.localPagination.current - 1,
727
+ pageSize: this.localPagination.pageSize
728
+ })
729
+ }
730
+ }}
731
+ >
732
+ &lt;
733
+ </div>
734
+
735
+ {/* 当前页按钮 */}
736
+ <div class="custom-pagination-btn active">{this.localPagination.current}</div>
737
+
738
+ {/* 下一页按钮 */}
739
+ <div
740
+ class={[
741
+ 'custom-pagination-btn',
742
+ {
743
+ disabled:
744
+ this.localPagination.current ===
745
+ (this.localPagination && this.localPagination.pageSize
746
+ ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
747
+ : 0) ||
748
+ !(this.localPagination && this.localPagination.pageSize
749
+ ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
750
+ : 0)
751
+ }
752
+ ]}
753
+ onClick={() => {
754
+ const totalPages =
755
+ this.localPagination && this.localPagination.pageSize
756
+ ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
757
+ : 0
758
+ if (totalPages > 0 && this.localPagination.current < totalPages) {
759
+ this.pageSize = this.localPagination.pageSize
760
+ this.pageNum = this.localPagination.current + 1
761
+ this.loadData({
762
+ current: this.localPagination.current + 1,
763
+ pageSize: this.localPagination.pageSize
764
+ })
765
+ }
766
+ }}
767
+ >
768
+ &gt;
769
+ </div>
770
+
771
+ {/* 末页按钮 */}
772
+ <div
773
+ class={[
774
+ 'custom-pagination-btn',
775
+ {
776
+ disabled:
777
+ this.localPagination.current ===
778
+ (this.localPagination && this.localPagination.pageSize
779
+ ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
780
+ : 0) ||
781
+ !(this.localPagination && this.localPagination.pageSize
782
+ ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
783
+ : 0)
784
+ }
785
+ ]}
786
+ onClick={() => {
787
+ const totalPages =
788
+ this.localPagination && this.localPagination.pageSize
789
+ ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
790
+ : 0
791
+ if (totalPages > 0 && this.localPagination.current < totalPages) {
792
+ this.pageSize = this.localPagination.pageSize
793
+ this.pageNum = totalPages
794
+ this.loadData({
795
+ current: totalPages,
796
+ pageSize: this.localPagination.pageSize
797
+ })
798
+ }
799
+ }}
800
+ >
801
+ <span class="custom-pagination-icon icon-rotate-left">⌅</span>
802
+ </div>
803
+
804
+ {/* 分页信息 */}
805
+ <div class="custom-pagination-info">
806
+
807
+ {this.localPagination && this.localPagination.pageSize
808
+ ? Math.ceil((this.localPagination.total || 0) / (this.localPagination.pageSize || 1))
809
+ : 0}
810
+ 页,{' '}
811
+ {this.localPagination && typeof this.localPagination.total !== 'undefined'
812
+ ? this.localPagination.total
813
+ : 0}
814
+
815
+ </div>
816
+ </div>
817
+ ) : (
818
+ <a-pagination
819
+ total={this.localPagination.total}
820
+ pageSize={this.localPagination.pageSize}
821
+ current={this.localPagination.current}
822
+ pageSizeOptions={this.pageSizeOptions}
823
+ onChange={(page, pageSize) => {
824
+ this.pageSize = pageSize
825
+ this.pageNum = page
826
+ this.loadData({
827
+ current: page,
828
+ pageSize: pageSize
829
+ })
830
+ }}
831
+ onShowSizeChange={(page, pageSize) => {
832
+ this.pageSize = pageSize
833
+ this.pageNum = page
834
+ this.loadData({
835
+ current: page,
836
+ pageSize: pageSize
837
+ })
838
+ }}
839
+ show-total={(total, range) =>
840
+ range[0] === range[1] ? `${range[0]} | 共 ${total} 项` : `${range[0]}-${range[1]} | 共 ${total} 项`
841
+ }
842
+ showSizeChanger={this.localPagination.showSizeChanger}
843
+ />
844
+ )}
845
+ </a-col>
846
+ </a-row>
847
+ )
848
+ const forwardedScopedSlots = { ...(this.$scopedSlots || {}) }
849
+ if (!this.expandedRowEnabled) {
850
+ delete forwardedScopedSlots.expandedRowRender
851
+ }
852
+ const table = (
853
+ <a-table
854
+ {...{ props, scopedSlots: forwardedScopedSlots }}
855
+ onChange={this.loadData}
856
+ onExpand={(expanded, record) => this.onExpand(expanded, record)}
857
+ >
858
+ {Object.keys(this.$slots).map(name => (
859
+ <template slot={name}>{this.$slots[name]}</template>
860
+ ))}
861
+ </a-table>
862
+ )
863
+ return (
864
+ <div class="table-wrapper">
865
+ {showAlert && this.showSelected && this.selectRowMode === 'default' ? this.renderAlert() : null}
866
+ {table}
867
+ {!this.hidePagination ? pagination : null}
868
+ </div>
869
+ )
870
+ }
871
+ }
872
+
873
+ // 添加自定义分页样式
874
+ const customPaginationStyles = `
875
+ <style>
876
+ .custom-pagination-container {
877
+ display: flex;
878
+ align-items: center;
879
+ gap: 8px;
880
+ }
881
+
882
+ .custom-pagination-btn {
883
+ width: 32px;
884
+ height: 32px;
885
+ border: 1px solid #d9d9d9;
886
+ border-radius: 6px;
887
+ background: #fafafa;
888
+ color: #5D5C5C;
889
+ font-size: 14px;
890
+ font-weight: 500;
891
+ display: flex;
892
+ align-items: center;
893
+ justify-content: center;
894
+ cursor: pointer;
895
+ transition: all 0.3s;
896
+ user-select: none;
897
+ }
898
+
899
+ .custom-pagination-btn:hover {
900
+ border-color: #0057FE;
901
+ color: #0057FE;
902
+ }
903
+
904
+ .custom-pagination-btn.active {
905
+ background: #0057FE;
906
+ border-color: #0057FE;
907
+ color: #fff;
908
+ }
909
+
910
+ .custom-pagination-btn.disabled {
911
+ background: #f5f5f5;
912
+ border-color: #d9d9d9;
913
+ color: #bfbfbf;
914
+ cursor: not-allowed;
915
+ }
916
+
917
+ .custom-pagination-btn.disabled:hover {
918
+ border-color: #d9d9d9;
919
+ color: #bfbfbf;
920
+ }
921
+
922
+ .custom-pagination-info {
923
+ margin-left: 16px;
924
+ color: #666;
925
+ font-size: 14px;
926
+ white-space: nowrap;
927
+ }
928
+
929
+ /* 自定义图标与旋转方向 */
930
+ .custom-pagination-icon {
931
+ display: inline-block;
932
+ line-height: 1;
933
+ transform-origin: center;
934
+ }
935
+ .icon-rotate-left {
936
+ transform: rotate(90deg);
937
+ }
938
+ .icon-rotate-right {
939
+ transform: rotate(-90deg);
940
+ }
941
+ </style>
942
+ `
943
+
944
+ // 将样式注入到页面
945
+ if (typeof document !== 'undefined') {
946
+ const styleId = 'custom-pagination-styles'
947
+ if (!document.getElementById(styleId)) {
948
+ const styleElement = document.createElement('style')
949
+ styleElement.id = styleId
950
+ styleElement.textContent = customPaginationStyles.replace('<style>', '').replace('</style>', '')
951
+ document.head.appendChild(styleElement)
952
+ }
953
+ }