@idooel/components 0.0.2-beta.29 → 0.0.2-beta.30
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.
- package/dist/@idooel/components.esm.js +463 -40
- package/dist/@idooel/components.umd.js +463 -40
- package/package.json +1 -1
- package/packages/models/tree-table-model/README.md +0 -0
- package/packages/models/tree-table-model/src/index.vue +131 -10
- package/packages/table/src/index.vue +329 -22
- package/packages/utils/README.md +172 -0
package/package.json
CHANGED
|
File without changes
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
:pageSizeOptions="pageSizeOptions"
|
|
46
46
|
:data-source="tableData"
|
|
47
47
|
:mode="mode"
|
|
48
|
+
v-bind="pageConfig"
|
|
48
49
|
@change-page="onChangePage"
|
|
49
50
|
></ele-table>
|
|
50
51
|
</section>
|
|
@@ -128,6 +129,7 @@ export default {
|
|
|
128
129
|
total: 0,
|
|
129
130
|
tableQuerys: {},
|
|
130
131
|
resizeObserverModelTableWrapper: null,
|
|
132
|
+
resizeObserverModelTableContainer: null,
|
|
131
133
|
modelTableWrapperHeight: 0,
|
|
132
134
|
currentTreeNodeData: {},
|
|
133
135
|
currentRowData: {},
|
|
@@ -323,6 +325,15 @@ export default {
|
|
|
323
325
|
const { page = {} } = this.tableMeta
|
|
324
326
|
return page.pageSize || 10
|
|
325
327
|
},
|
|
328
|
+
nextCursorConfig () {
|
|
329
|
+
if (this.mode == 'next-cursor') {
|
|
330
|
+
const { page = {} } = this.tableMeta
|
|
331
|
+
const { nextCursor = {} } = page
|
|
332
|
+
return nextCursor
|
|
333
|
+
} else {
|
|
334
|
+
return void 0
|
|
335
|
+
}
|
|
336
|
+
},
|
|
326
337
|
mode () {
|
|
327
338
|
const { page = {} } = this.tableMeta
|
|
328
339
|
return page.mode
|
|
@@ -706,6 +717,17 @@ export default {
|
|
|
706
717
|
}
|
|
707
718
|
})
|
|
708
719
|
})
|
|
720
|
+
if (this.nextCursorConfig) {
|
|
721
|
+
const { count: { url: countUrl, requestType = 'GET', params = {}, fieldMap = {} } } = this.nextCursorConfig
|
|
722
|
+
const countRet = await net[requestType.toLowerCase()](
|
|
723
|
+
countUrl,
|
|
724
|
+
Object.assign({}, this.tableQuerys, params)
|
|
725
|
+
).then(resp => {
|
|
726
|
+
const { data = 0 } = resp || {}
|
|
727
|
+
return data
|
|
728
|
+
})
|
|
729
|
+
this.total = countRet
|
|
730
|
+
}
|
|
709
731
|
this.cleanCurrentModelEffect(false) // 不清空 currentRowData,除非明确需要
|
|
710
732
|
this.tableData = ret
|
|
711
733
|
return ret
|
|
@@ -713,23 +735,51 @@ export default {
|
|
|
713
735
|
calculateTableHeight () {
|
|
714
736
|
const currentViewportHeight = window.innerHeight
|
|
715
737
|
const tableRef = this.$refs[this.tableRef]
|
|
738
|
+
if (!tableRef || !tableRef.$el) return
|
|
739
|
+
|
|
716
740
|
const { top: tableToTop, width } = tableRef.$el.getBoundingClientRect()
|
|
717
741
|
this.tableWidth = width
|
|
718
|
-
|
|
742
|
+
|
|
743
|
+
// 获取分页组件的高度(如果存在)
|
|
744
|
+
let paginationHeight = 0
|
|
745
|
+
const paginationEl = tableRef.$el.querySelector('.g-table__pagination')
|
|
746
|
+
if (paginationEl) {
|
|
747
|
+
paginationHeight = paginationEl.getBoundingClientRect().height || 50
|
|
748
|
+
} else {
|
|
749
|
+
// 如果分页组件还未渲染,使用默认高度
|
|
750
|
+
paginationHeight = 50
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// 计算表格高度:视口高度 - 表格顶部距离 - 分页高度 - 额外高度
|
|
754
|
+
const calculatedHeight = currentViewportHeight - tableToTop - paginationHeight - this.overHeight - 20
|
|
755
|
+
// 确保最小高度,避免表格过小
|
|
756
|
+
this.tableHeight = Math.max(calculatedHeight, 200)
|
|
719
757
|
},
|
|
720
758
|
calculateTreeHeight () {
|
|
721
759
|
if (!this.showTree) return
|
|
722
760
|
const modelTableContainerRef = this.$refs[this.modelTableContainerRef]
|
|
761
|
+
if (!modelTableContainerRef) return
|
|
762
|
+
|
|
723
763
|
const { height } = modelTableContainerRef.getBoundingClientRect()
|
|
764
|
+
// 确保树的高度和表格容器高度一致
|
|
724
765
|
this.treeWrapperHeight = height
|
|
766
|
+
|
|
767
|
+
// 如果表格容器有标题,需要减去标题高度
|
|
768
|
+
const titleEl = modelTableContainerRef.querySelector('.model__table--title')
|
|
769
|
+
if (titleEl) {
|
|
770
|
+
const titleHeight = titleEl.getBoundingClientRect().height
|
|
771
|
+
this.treeWrapperHeight = height - titleHeight
|
|
772
|
+
}
|
|
725
773
|
},
|
|
726
774
|
async keepAliveRefresh () {
|
|
727
775
|
// 重新计算表格高度(应对窗口大小变化)
|
|
728
776
|
this.$nextTick(() => {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
this.
|
|
732
|
-
|
|
777
|
+
setTimeout(() => {
|
|
778
|
+
this.calculateTableHeight()
|
|
779
|
+
if (this.showTree) {
|
|
780
|
+
this.calculateTreeHeight()
|
|
781
|
+
}
|
|
782
|
+
}, 200)
|
|
733
783
|
})
|
|
734
784
|
// 刷新列表数据
|
|
735
785
|
const { overrideInit = false } = this.tableMeta
|
|
@@ -743,21 +793,74 @@ export default {
|
|
|
743
793
|
}
|
|
744
794
|
},
|
|
745
795
|
mounted () {
|
|
746
|
-
|
|
796
|
+
// 初始化时先设置一个默认高度,避免布局混乱
|
|
797
|
+
this.tableHeight = 400
|
|
798
|
+
if (this.showTree) {
|
|
799
|
+
this.treeWrapperHeight = 400
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// 延迟计算,确保所有组件都已渲染
|
|
747
803
|
this.$nextTick(() => {
|
|
748
|
-
|
|
804
|
+
setTimeout(() => {
|
|
805
|
+
this.calculateTableHeight()
|
|
806
|
+
this.calculateTreeHeight()
|
|
807
|
+
}, 200)
|
|
749
808
|
})
|
|
809
|
+
|
|
810
|
+
// 使用 ResizeObserver 监听容器大小变化
|
|
750
811
|
this.resizeObserverModelTableWrapper = new ResizeObserver(entries => {
|
|
751
812
|
for (const _ of entries) {
|
|
752
813
|
requestAnimationFrame(() => {
|
|
753
|
-
|
|
814
|
+
// 延迟重新计算,确保分页组件高度已更新
|
|
815
|
+
setTimeout(() => {
|
|
816
|
+
this.calculateTableHeight()
|
|
817
|
+
if (this.showTree) {
|
|
818
|
+
this.calculateTreeHeight()
|
|
819
|
+
}
|
|
820
|
+
}, 100)
|
|
754
821
|
})
|
|
755
822
|
}
|
|
756
823
|
})
|
|
757
|
-
|
|
824
|
+
|
|
825
|
+
if (this.$refs[this.modelTableWrapper]) {
|
|
826
|
+
this.resizeObserverModelTableWrapper.observe(this.$refs[this.modelTableWrapper])
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// 监听表格容器大小变化(用于同步树高度)
|
|
830
|
+
if (this.showTree && this.$refs[this.modelTableContainerRef]) {
|
|
831
|
+
this.resizeObserverModelTableContainer = new ResizeObserver(entries => {
|
|
832
|
+
for (const _ of entries) {
|
|
833
|
+
requestAnimationFrame(() => {
|
|
834
|
+
this.calculateTreeHeight()
|
|
835
|
+
})
|
|
836
|
+
}
|
|
837
|
+
})
|
|
838
|
+
this.resizeObserverModelTableContainer.observe(this.$refs[this.modelTableContainerRef])
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// 监听窗口大小变化
|
|
842
|
+
this.handleResize = () => {
|
|
843
|
+
this.$nextTick(() => {
|
|
844
|
+
setTimeout(() => {
|
|
845
|
+
this.calculateTableHeight()
|
|
846
|
+
if (this.showTree) {
|
|
847
|
+
this.calculateTreeHeight()
|
|
848
|
+
}
|
|
849
|
+
}, 100)
|
|
850
|
+
})
|
|
851
|
+
}
|
|
852
|
+
window.addEventListener('resize', this.handleResize)
|
|
758
853
|
},
|
|
759
854
|
destroyed () {
|
|
760
|
-
this.resizeObserverModelTableWrapper
|
|
855
|
+
if (this.resizeObserverModelTableWrapper) {
|
|
856
|
+
this.resizeObserverModelTableWrapper.disconnect()
|
|
857
|
+
}
|
|
858
|
+
if (this.resizeObserverModelTableContainer) {
|
|
859
|
+
this.resizeObserverModelTableContainer.disconnect()
|
|
860
|
+
}
|
|
861
|
+
if (this.handleResize) {
|
|
862
|
+
window.removeEventListener('resize', this.handleResize)
|
|
863
|
+
}
|
|
761
864
|
if (this.model) {
|
|
762
865
|
// 清理订阅
|
|
763
866
|
if (this.unsubscribe) {
|
|
@@ -785,7 +888,12 @@ export default {
|
|
|
785
888
|
display: flex;
|
|
786
889
|
flex-direction: row;
|
|
787
890
|
width: 100%;
|
|
891
|
+
height: 100%;
|
|
892
|
+
overflow: hidden;
|
|
788
893
|
.model__tree-table--container {
|
|
894
|
+
display: flex;
|
|
895
|
+
flex-direction: column;
|
|
896
|
+
height: 100%;
|
|
789
897
|
.model__tree--wrapper {
|
|
790
898
|
width: 240px;
|
|
791
899
|
background: #fff;
|
|
@@ -794,12 +902,17 @@ export default {
|
|
|
794
902
|
box-sizing: border-box;
|
|
795
903
|
margin-right: 16px;
|
|
796
904
|
overflow-y: auto;
|
|
905
|
+
overflow-x: hidden;
|
|
797
906
|
}
|
|
798
907
|
}
|
|
799
908
|
.model__table--container {
|
|
800
909
|
width: 100%;
|
|
801
910
|
min-width: 0;
|
|
802
911
|
background: #fff;
|
|
912
|
+
display: flex;
|
|
913
|
+
flex-direction: column;
|
|
914
|
+
height: 100%;
|
|
915
|
+
overflow: hidden;
|
|
803
916
|
.model__table--title {
|
|
804
917
|
.model__table-title--bar {
|
|
805
918
|
width: 100%;
|
|
@@ -820,6 +933,10 @@ export default {
|
|
|
820
933
|
}
|
|
821
934
|
.model__table--wrapper {
|
|
822
935
|
background: #fff;
|
|
936
|
+
display: flex;
|
|
937
|
+
flex-direction: column;
|
|
938
|
+
height: 100%;
|
|
939
|
+
overflow: hidden;
|
|
823
940
|
.button-row__area {
|
|
824
941
|
width: 100%;
|
|
825
942
|
display: flex;
|
|
@@ -829,8 +946,12 @@ export default {
|
|
|
829
946
|
padding-top: 16px;
|
|
830
947
|
padding-bottom: 8px;
|
|
831
948
|
padding-right: 16px;
|
|
949
|
+
flex-shrink: 0;
|
|
832
950
|
}
|
|
833
951
|
.g-table__wrapper {
|
|
952
|
+
flex: 1;
|
|
953
|
+
min-height: 0;
|
|
954
|
+
overflow: hidden;
|
|
834
955
|
.fsm {
|
|
835
956
|
cursor: pointer;
|
|
836
957
|
color: var(--idooel-primary-color);
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="g-table__wrapper" :style="wrapperStyle">
|
|
2
|
+
<div class="g-table__wrapper" ref="tableWrapper" :style="wrapperStyle">
|
|
3
3
|
<a-table
|
|
4
|
+
:key="tableRenderKey"
|
|
4
5
|
:bordered="bordered"
|
|
5
6
|
:class="[isNoData && 'g-table__no-data']"
|
|
6
7
|
:pagination="false"
|
|
7
8
|
:loading="loading"
|
|
8
9
|
size="middle"
|
|
9
|
-
:columns="
|
|
10
|
-
:row-selection="
|
|
10
|
+
:columns="smartColumns"
|
|
11
|
+
:row-selection="smartRowSelection"
|
|
11
12
|
:row-class-name="setRowClassName"
|
|
12
13
|
:data-source="dataSource"
|
|
13
|
-
:scroll="
|
|
14
|
+
:scroll="smartScroll">
|
|
14
15
|
<template slot="action" slot-scope="record">
|
|
15
16
|
<Actions v-on="$listeners" :data-source="actions" :record="record"></Actions>
|
|
16
17
|
</template>
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
:pageSize="innerPageSize"
|
|
34
35
|
:current="innerCurrentPage"
|
|
35
36
|
:pageSizeOptions="pageSizeOptions"
|
|
37
|
+
:data="dataSource.length"
|
|
36
38
|
@change="onChangePagination"
|
|
37
39
|
@showSizeChange="onShowSizeChange"
|
|
38
40
|
:total="total"
|
|
@@ -64,7 +66,8 @@ export default {
|
|
|
64
66
|
type: Number
|
|
65
67
|
},
|
|
66
68
|
x: {
|
|
67
|
-
|
|
69
|
+
// ant-design-vue: scroll.x 支持 number | string(如 'max-content')
|
|
70
|
+
type: [Number, String],
|
|
68
71
|
default: 1200
|
|
69
72
|
},
|
|
70
73
|
y: {
|
|
@@ -117,7 +120,11 @@ export default {
|
|
|
117
120
|
innerPageSize: 10,
|
|
118
121
|
innerCurrentPage: 1,
|
|
119
122
|
tableContentHeight: 0,
|
|
120
|
-
obs: []
|
|
123
|
+
obs: [],
|
|
124
|
+
// 容器宽度,用于智能判断是否需要 fixed 列
|
|
125
|
+
containerWidth: 0,
|
|
126
|
+
// 用于强制重新渲染表格(当 fixed 列状态切换时)
|
|
127
|
+
tableRenderKey: 0
|
|
121
128
|
}
|
|
122
129
|
},
|
|
123
130
|
computed: {
|
|
@@ -152,12 +159,114 @@ export default {
|
|
|
152
159
|
isFlexColumn () {
|
|
153
160
|
return this.columns.every(col => !col.width)
|
|
154
161
|
},
|
|
162
|
+
/**
|
|
163
|
+
* 计算所有列的总宽度(包括 rowSelection 的 checkbox 列和操作列)
|
|
164
|
+
*/
|
|
165
|
+
totalColumnsWidth () {
|
|
166
|
+
const cols = this.innerColumns || []
|
|
167
|
+
let total = cols.reduce((sum, col) => {
|
|
168
|
+
const w = col && col.width
|
|
169
|
+
return sum + (typeof w === 'number' ? w : 0)
|
|
170
|
+
}, 0)
|
|
171
|
+
// rowSelection 的 checkbox/radio 列,antd 默认约 60px
|
|
172
|
+
if (this.rowSelection) total += 60
|
|
173
|
+
// 操作列(operations)的宽度
|
|
174
|
+
if (this.operations && this.operations.width && typeof this.operations.width === 'number') {
|
|
175
|
+
total += this.operations.width
|
|
176
|
+
}
|
|
177
|
+
return total
|
|
178
|
+
},
|
|
179
|
+
/**
|
|
180
|
+
* 是否需要横向滚动:容器宽度 < 列总宽度
|
|
181
|
+
*/
|
|
182
|
+
needHorizontalScroll () {
|
|
183
|
+
// 未获取容器宽度前,先假定需要滚动(保守策略)
|
|
184
|
+
if (!this.containerWidth) return true
|
|
185
|
+
// 加一点容差
|
|
186
|
+
return this.containerWidth < this.totalColumnsWidth - 5
|
|
187
|
+
},
|
|
188
|
+
/**
|
|
189
|
+
* 智能列配置:
|
|
190
|
+
* - 当需要横向滚动时,保留原始 fixed 属性
|
|
191
|
+
* - 当容器足够宽时,移除 fixed 属性,让表格自动铺满
|
|
192
|
+
*/
|
|
193
|
+
smartColumns () {
|
|
194
|
+
if (this.needHorizontalScroll) {
|
|
195
|
+
// 需要滚动,保留原始配置
|
|
196
|
+
return this.innerColumns
|
|
197
|
+
}
|
|
198
|
+
// 不需要滚动,移除所有 fixed 属性
|
|
199
|
+
return this.innerColumns.map(col => {
|
|
200
|
+
if (col.fixed) {
|
|
201
|
+
const { fixed, ...rest } = col
|
|
202
|
+
return rest
|
|
203
|
+
}
|
|
204
|
+
return col
|
|
205
|
+
})
|
|
206
|
+
},
|
|
207
|
+
/**
|
|
208
|
+
* 智能 scroll 配置:
|
|
209
|
+
* - 当需要横向滚动时,设置 scroll.x
|
|
210
|
+
* - 当容器足够宽时,不设置 scroll.x,避免产生空白区域
|
|
211
|
+
*/
|
|
212
|
+
smartScroll () {
|
|
213
|
+
if (!this.needHorizontalScroll) {
|
|
214
|
+
// 不需要横向滚动,只保留 y 方向(如果需要)
|
|
215
|
+
if (this.height && this.needScrollY) {
|
|
216
|
+
const availableHeight = this.tableHeaderHeight && this.paginationHeight
|
|
217
|
+
? this.getScrollHeightByHeight
|
|
218
|
+
: this.height - 100
|
|
219
|
+
if (availableHeight > 50) {
|
|
220
|
+
return { y: availableHeight }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {}
|
|
224
|
+
}
|
|
225
|
+
// 需要横向滚动,使用原有逻辑
|
|
226
|
+
return this.getScroll
|
|
227
|
+
},
|
|
228
|
+
/**
|
|
229
|
+
* 智能 rowSelection 配置:
|
|
230
|
+
* - 当需要横向滚动时,保留原始 fixed 属性
|
|
231
|
+
* - 当容器足够宽时,移除 fixed 属性
|
|
232
|
+
*/
|
|
233
|
+
smartRowSelection () {
|
|
234
|
+
if (!this.rowSelection) return null
|
|
235
|
+
if (this.needHorizontalScroll) {
|
|
236
|
+
return this.rowSelection
|
|
237
|
+
}
|
|
238
|
+
// 不需要滚动,移除 fixed 属性
|
|
239
|
+
if (this.rowSelection.fixed) {
|
|
240
|
+
const { fixed, ...rest } = this.rowSelection
|
|
241
|
+
return rest
|
|
242
|
+
}
|
|
243
|
+
return this.rowSelection
|
|
244
|
+
},
|
|
155
245
|
getScroll () {
|
|
156
246
|
if (this.scroll) {
|
|
157
247
|
return this.scroll
|
|
158
248
|
} else {
|
|
159
249
|
// 固定列需要 scroll.x 才能正确同步行高,始终设置一个有效值
|
|
160
|
-
|
|
250
|
+
let baseX = (this.x === '' || this.x === null || this.x === undefined) ? 1200 : this.x
|
|
251
|
+
|
|
252
|
+
// 解决“x 给太大导致操作列前出现大块空白”的问题:
|
|
253
|
+
// 当所有列都给了明确 width 时,scroll.x 取列宽总和最合理;大于总和会产生多余区域。
|
|
254
|
+
if (typeof baseX === 'number') {
|
|
255
|
+
const cols = this.innerColumns || []
|
|
256
|
+
const total = cols.reduce((sum, col) => {
|
|
257
|
+
const w = col && col.width
|
|
258
|
+
return sum + (typeof w === 'number' ? w : 0)
|
|
259
|
+
}, 0)
|
|
260
|
+
|
|
261
|
+
// rowSelection 的 checkbox/radio 列是 antd 自动加的,给一个经验宽度避免误差
|
|
262
|
+
const selectionWidth = this.rowSelection ? 60 : 0
|
|
263
|
+
|
|
264
|
+
// 只有当所有列都明确给了宽度(total > 0)时才 clamp
|
|
265
|
+
if (total > 0) {
|
|
266
|
+
const minX = total + selectionWidth
|
|
267
|
+
if (baseX > minX) baseX = minX
|
|
268
|
+
}
|
|
269
|
+
}
|
|
161
270
|
|
|
162
271
|
if (this.height && this.needScrollY) {
|
|
163
272
|
const availableHeight = this.tableHeaderHeight && this.paginationHeight
|
|
@@ -178,6 +287,20 @@ export default {
|
|
|
178
287
|
this.innerPageSize = pageSize
|
|
179
288
|
},
|
|
180
289
|
immediate: true
|
|
290
|
+
},
|
|
291
|
+
/**
|
|
292
|
+
* 监听 needHorizontalScroll 变化,强制重新渲染表格
|
|
293
|
+
* 当从"需要固定列"切换到"不需要固定列"或反之时,antd 的 a-table 需要完全重新渲染
|
|
294
|
+
*/
|
|
295
|
+
needHorizontalScroll (newVal, oldVal) {
|
|
296
|
+
if (newVal !== oldVal) {
|
|
297
|
+
// 更新 key 强制 Vue 销毁并重建 a-table 组件
|
|
298
|
+
this.tableRenderKey++
|
|
299
|
+
// 使用重试机制确保固定列完全渲染
|
|
300
|
+
this.$nextTick(() => {
|
|
301
|
+
this.retrySyncFixedColumns(newVal)
|
|
302
|
+
})
|
|
303
|
+
}
|
|
181
304
|
}
|
|
182
305
|
},
|
|
183
306
|
methods: {
|
|
@@ -215,54 +338,238 @@ export default {
|
|
|
215
338
|
this.innerCurrentPage = page
|
|
216
339
|
this.innerPageSize = pageSize
|
|
217
340
|
this.$emit('change-page', page, pageSize)
|
|
341
|
+
},
|
|
342
|
+
syncFixedColumns () {
|
|
343
|
+
// 强制 ant-design-vue 重新计算固定列的宽度
|
|
344
|
+
this.$nextTick(() => {
|
|
345
|
+
const tableEl = this.$el.querySelector('.ant-table')
|
|
346
|
+
if (tableEl) {
|
|
347
|
+
// 触发窗口 resize 事件,让 ant-design-vue 重新计算
|
|
348
|
+
window.dispatchEvent(new Event('resize'))
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
},
|
|
352
|
+
/**
|
|
353
|
+
* 带重试的固定列同步机制
|
|
354
|
+
* antd 的 a-table 在 key 变化后重新渲染固定列需要一定时间,
|
|
355
|
+
* 这里通过重试确保固定列渲染完成后再同步。
|
|
356
|
+
* @param {boolean} needFixed - 是否需要固定列
|
|
357
|
+
* @param {number} retries - 当前重试次数
|
|
358
|
+
*/
|
|
359
|
+
retrySyncFixedColumns (needFixed, retries = 0) {
|
|
360
|
+
const MAX_RETRIES = 5
|
|
361
|
+
const DELAY = 100 // 每次延迟 100ms
|
|
362
|
+
|
|
363
|
+
setTimeout(() => {
|
|
364
|
+
this.syncFixedColumns()
|
|
365
|
+
this.syncHeaderTableWidth()
|
|
366
|
+
this.bindScrollSync()
|
|
367
|
+
|
|
368
|
+
// 如果需要固定列,检查是否已渲染
|
|
369
|
+
if (needFixed && retries < MAX_RETRIES) {
|
|
370
|
+
const hasFixedLeft = this.$el.querySelector('.ant-table-fixed-left .ant-table-body tbody tr')
|
|
371
|
+
const hasFixedRight = this.$el.querySelector('.ant-table-fixed-right .ant-table-body tbody tr')
|
|
372
|
+
|
|
373
|
+
// 如果有固定列配置但还没渲染出来,继续重试
|
|
374
|
+
const hasFixedConfig = this.innerColumns.some(c => c.fixed) || (this.rowSelection && this.rowSelection.fixed)
|
|
375
|
+
if (hasFixedConfig && !hasFixedLeft && !hasFixedRight) {
|
|
376
|
+
this.retrySyncFixedColumns(needFixed, retries + 1)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}, DELAY)
|
|
380
|
+
},
|
|
381
|
+
/**
|
|
382
|
+
* 修复 x: 'max-content' 场景下表头不跟着横向滚动的问题。
|
|
383
|
+
* 原因:ant-design-vue 的 header table 和 body table 各自算 max-content,
|
|
384
|
+
* header 按表头文字算、body 按实际数据算,二者宽度不同时 header 就"滚不动"。
|
|
385
|
+
* 方案:数据渲染后把 header table 的 width 强制设成和 body table 一样。
|
|
386
|
+
*/
|
|
387
|
+
syncHeaderTableWidth () {
|
|
388
|
+
this.$nextTick(() => {
|
|
389
|
+
const headerTable = this.$el.querySelector('.ant-table-scroll .ant-table-header table')
|
|
390
|
+
const bodyTable = this.$el.querySelector('.ant-table-scroll .ant-table-body table')
|
|
391
|
+
if (!headerTable || !bodyTable) return
|
|
392
|
+
|
|
393
|
+
const bodyW = bodyTable.getBoundingClientRect().width
|
|
394
|
+
const headerW = headerTable.getBoundingClientRect().width
|
|
395
|
+
|
|
396
|
+
// 始终同步表头宽度到表体宽度,确保窗口变大/变小时都能正确响应
|
|
397
|
+
// 只有当宽度差异超过 2px 时才更新,避免频繁设置样式
|
|
398
|
+
if (Math.abs(bodyW - headerW) > 2) {
|
|
399
|
+
headerTable.style.width = `${bodyW}px`
|
|
400
|
+
headerTable.style.minWidth = `${bodyW}px`
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
},
|
|
404
|
+
/**
|
|
405
|
+
* 监听表体横向滚动,同步到表头(防止 antd 自带同步失效)
|
|
406
|
+
*/
|
|
407
|
+
bindScrollSync () {
|
|
408
|
+
const body = this.$el.querySelector('.ant-table-scroll .ant-table-body')
|
|
409
|
+
const header = this.$el.querySelector('.ant-table-scroll .ant-table-header')
|
|
410
|
+
if (!body || !header) return
|
|
411
|
+
|
|
412
|
+
if (this._scrollHandler) return // 已绑定
|
|
413
|
+
this._scrollHandler = () => {
|
|
414
|
+
header.scrollLeft = body.scrollLeft
|
|
415
|
+
}
|
|
416
|
+
body.addEventListener('scroll', this._scrollHandler, { passive: true })
|
|
417
|
+
},
|
|
418
|
+
unbindScrollSync () {
|
|
419
|
+
const body = this.$el.querySelector('.ant-table-scroll .ant-table-body')
|
|
420
|
+
if (body && this._scrollHandler) {
|
|
421
|
+
body.removeEventListener('scroll', this._scrollHandler)
|
|
422
|
+
this._scrollHandler = null
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
/**
|
|
426
|
+
* 测量容器宽度
|
|
427
|
+
*/
|
|
428
|
+
measureContainerWidth () {
|
|
429
|
+
const wrapper = this.$refs.tableWrapper
|
|
430
|
+
if (wrapper) {
|
|
431
|
+
this.containerWidth = wrapper.clientWidth
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
/**
|
|
435
|
+
* 使用 ResizeObserver 监听容器宽度变化
|
|
436
|
+
*/
|
|
437
|
+
observeContainerWidth () {
|
|
438
|
+
const wrapper = this.$refs.tableWrapper
|
|
439
|
+
if (!wrapper || typeof ResizeObserver === 'undefined') return
|
|
440
|
+
|
|
441
|
+
if (this._containerResizeObserver) return // 已绑定
|
|
442
|
+
|
|
443
|
+
this._containerResizeObserver = new ResizeObserver((entries) => {
|
|
444
|
+
for (const entry of entries) {
|
|
445
|
+
const newWidth = entry.contentRect.width
|
|
446
|
+
// 只有宽度变化超过阈值才触发更新,避免微小变化导致频繁重渲染
|
|
447
|
+
if (Math.abs(newWidth - this.containerWidth) > 10) {
|
|
448
|
+
const oldNeedScroll = this.needHorizontalScroll
|
|
449
|
+
this.containerWidth = newWidth
|
|
450
|
+
// 容器宽度变化会触发 needHorizontalScroll 的重新计算
|
|
451
|
+
// needHorizontalScroll 的 watcher 会处理表格重新渲染和列宽同步
|
|
452
|
+
// 这里不需要直接调用 syncFixedColumns,避免与 watcher 的执行时机冲突
|
|
453
|
+
// 无论 needHorizontalScroll 是否变化,都立即同步表头宽度
|
|
454
|
+
// 确保窗口变大/变小时表头都能及时响应
|
|
455
|
+
this.$nextTick(() => {
|
|
456
|
+
// 使用 requestAnimationFrame 确保在浏览器重绘后同步表头宽度
|
|
457
|
+
// 这样能确保表体宽度已经更新完成
|
|
458
|
+
requestAnimationFrame(() => {
|
|
459
|
+
this.syncHeaderTableWidth()
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
// 如果 needHorizontalScroll 状态没有变化,说明只是列宽需要调整,直接同步
|
|
463
|
+
// 如果状态变化了,watcher 会处理重新渲染
|
|
464
|
+
if (this.needHorizontalScroll === oldNeedScroll) {
|
|
465
|
+
// 防抖:延迟同步固定列,避免频繁调用
|
|
466
|
+
if (this._resizeDebounceTimer) {
|
|
467
|
+
clearTimeout(this._resizeDebounceTimer)
|
|
468
|
+
}
|
|
469
|
+
this._resizeDebounceTimer = setTimeout(() => {
|
|
470
|
+
this.syncFixedColumns()
|
|
471
|
+
this.syncHeaderTableWidth()
|
|
472
|
+
this.bindScrollSync()
|
|
473
|
+
}, 150)
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
this._containerResizeObserver.observe(wrapper)
|
|
480
|
+
},
|
|
481
|
+
/**
|
|
482
|
+
* 断开容器宽度监听
|
|
483
|
+
*/
|
|
484
|
+
unobserveContainerWidth () {
|
|
485
|
+
if (this._containerResizeObserver) {
|
|
486
|
+
this._containerResizeObserver.disconnect()
|
|
487
|
+
this._containerResizeObserver = null
|
|
488
|
+
}
|
|
489
|
+
// 清理防抖定时器
|
|
490
|
+
if (this._resizeDebounceTimer) {
|
|
491
|
+
clearTimeout(this._resizeDebounceTimer)
|
|
492
|
+
this._resizeDebounceTimer = null
|
|
493
|
+
}
|
|
218
494
|
}
|
|
219
495
|
},
|
|
220
496
|
mounted() {
|
|
221
497
|
this.$nextTick(() => {
|
|
498
|
+
// 先测量容器宽度,用于智能判断是否需要 fixed 列
|
|
499
|
+
this.measureContainerWidth()
|
|
500
|
+
this.observeContainerWidth()
|
|
501
|
+
|
|
222
502
|
this.setPaginationHeight()
|
|
223
503
|
setTimeout(() => {
|
|
224
504
|
this.setTableTbodyHeight()
|
|
225
505
|
this.setPaginationHeight()
|
|
506
|
+
// 强制同步固定列和主表的列宽
|
|
507
|
+
this.syncFixedColumns()
|
|
508
|
+
// 同步表头 table 宽度(修复 max-content 场景)
|
|
509
|
+
this.syncHeaderTableWidth()
|
|
510
|
+
// 绑定横向滚动同步
|
|
511
|
+
this.bindScrollSync()
|
|
226
512
|
}, 200)
|
|
227
513
|
})
|
|
514
|
+
|
|
515
|
+
// 监听数据变化,重新同步列宽
|
|
516
|
+
this.$watch('dataSource', () => {
|
|
517
|
+
this.$nextTick(() => {
|
|
518
|
+
setTimeout(() => {
|
|
519
|
+
this.syncFixedColumns()
|
|
520
|
+
this.syncHeaderTableWidth()
|
|
521
|
+
this.bindScrollSync()
|
|
522
|
+
}, 100)
|
|
523
|
+
})
|
|
524
|
+
}, { deep: true })
|
|
228
525
|
},
|
|
229
526
|
destroyed () {
|
|
230
527
|
this.obs.forEach(ob => ob.disconnect())
|
|
528
|
+
this.unbindScrollSync()
|
|
529
|
+
this.unobserveContainerWidth()
|
|
231
530
|
}
|
|
232
531
|
}
|
|
233
532
|
</script>
|
|
234
533
|
|
|
235
534
|
<style lang="scss" scoped>
|
|
236
535
|
.g-table__wrapper {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
536
|
+
/**
|
|
537
|
+
* 修复"宽屏下表格两侧出现空白"问题:
|
|
538
|
+
* 当视口宽度大于表格内容宽度时,主表(ant-table-scroll)不会自动拉伸,
|
|
539
|
+
* 而固定列(fixed-left/fixed-right)是 position:absolute 定位在容器边缘,中间就出现空白。
|
|
540
|
+
* 解决方案:让主表的 table 元素 min-width:100%,使其始终填满滚动容器。
|
|
541
|
+
*/
|
|
542
|
+
::v-deep .ant-table-scroll .ant-table-header table,
|
|
543
|
+
::v-deep .ant-table-scroll .ant-table-body table {
|
|
544
|
+
min-width: 100%;
|
|
243
545
|
}
|
|
244
546
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
547
|
+
/**
|
|
548
|
+
* 修复"固定列 + scroll.x"场景下,主表(ant-table-scroll)里会渲染一份 fixed 列的占位 header/cell。
|
|
549
|
+
* 这份占位本来只用于计算宽度,但在某些布局下会被看见,表现为"操作列前多了一大块空白/空列"。
|
|
550
|
+
* 这里用 visibility:hidden 隐藏占位(不影响占位宽度与 fixed 计算),避免视觉空白。
|
|
551
|
+
*/
|
|
552
|
+
::v-deep .ant-table-scroll .ant-table-header thead > tr > th.ant-table-fixed-columns-in-body.ant-table-row-cell-last,
|
|
553
|
+
::v-deep .ant-table-scroll .ant-table-body tbody > tr > td.ant-table-fixed-columns-in-body.ant-table-row-cell-last {
|
|
554
|
+
visibility: hidden;
|
|
251
555
|
}
|
|
252
556
|
|
|
253
|
-
/*
|
|
254
|
-
::v-deep .ant-table-
|
|
557
|
+
/* 强制统一行高,确保主表和固定列对齐 */
|
|
558
|
+
::v-deep .ant-table-tbody > tr > td {
|
|
255
559
|
height: 54px;
|
|
256
560
|
padding: 8px 16px;
|
|
257
561
|
vertical-align: middle;
|
|
258
562
|
box-sizing: border-box;
|
|
563
|
+
line-height: 38px;
|
|
259
564
|
}
|
|
260
565
|
|
|
261
|
-
|
|
566
|
+
/* 表头也统一高度和样式 */
|
|
567
|
+
::v-deep .ant-table-thead > tr > th {
|
|
262
568
|
height: 54px;
|
|
263
569
|
padding: 8px 16px;
|
|
264
570
|
vertical-align: middle;
|
|
265
571
|
box-sizing: border-box;
|
|
572
|
+
line-height: 38px;
|
|
266
573
|
}
|
|
267
574
|
|
|
268
575
|
/* 分页区域固定在底部 */
|