xt-element-ui 2.2.3 → 2.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +140 -192
  2. package/docs/README.md +162 -88
  3. package/docs/components/base/theme.md +248 -0
  4. package/docs/components/base/xt-list.md +1 -1
  5. package/docs/components/base/xt-scroll-arrow.md +87 -1
  6. package/docs/components/base/xt-scroll.md +237 -0
  7. package/docs/components/utils/config.md +18 -4
  8. package/docs/components/utils/theme.md +280 -183
  9. package/lib/index.common.1.js +4 -26
  10. package/lib/index.common.2.js +1 -1
  11. package/lib/index.common.js +1871 -671
  12. package/lib/index.css +1 -1
  13. package/lib/index.umd.1.js +4 -26
  14. package/lib/index.umd.2.js +1 -1
  15. package/lib/index.umd.js +1871 -671
  16. package/lib/index.umd.min.1.js +1 -1
  17. package/lib/index.umd.min.js +1 -1
  18. package/lib/theme/element-overrides.css +607 -0
  19. package/lib/theme/element-vars.css +280 -0
  20. package/lib/xt-badge/xt-badge.common.js +1 -1
  21. package/lib/xt-badge/xt-badge.umd.js +1 -1
  22. package/lib/xt-button/xt-button.common.js +1 -1
  23. package/lib/xt-button/xt-button.umd.js +1 -1
  24. package/lib/xt-card/xt-card.common.js +1 -1
  25. package/lib/xt-card/xt-card.umd.js +1 -1
  26. package/lib/xt-card-item/xt-card-item.common.js +1 -1
  27. package/lib/xt-card-item/xt-card-item.umd.js +1 -1
  28. package/lib/xt-chart/xt-chart.common.js +5 -5
  29. package/lib/xt-chart/xt-chart.umd.js +5 -5
  30. package/lib/xt-config-provider/xt-config-provider.css +1 -1
  31. package/lib/xt-date-picker/xt-date-picker.common.1.js +4 -26
  32. package/lib/xt-date-picker/xt-date-picker.common.2.js +1 -1
  33. package/lib/xt-date-picker/xt-date-picker.common.js +1 -47
  34. package/lib/xt-date-picker/xt-date-picker.css +1 -1
  35. package/lib/xt-date-picker/xt-date-picker.umd.1.js +4 -26
  36. package/lib/xt-date-picker/xt-date-picker.umd.2.js +1 -1
  37. package/lib/xt-date-picker/xt-date-picker.umd.js +1 -47
  38. package/lib/xt-date-picker/xt-date-picker.umd.min.1.js +1 -1
  39. package/lib/xt-date-picker/xt-date-picker.umd.min.js +1 -1
  40. package/lib/xt-flex-box/xt-flex-box.common.js +1 -1
  41. package/lib/xt-flex-box/xt-flex-box.umd.js +1 -1
  42. package/lib/xt-flow/xt-flow.common.js +1 -1
  43. package/lib/xt-flow/xt-flow.umd.js +1 -1
  44. package/lib/xt-form-schema/xt-form-schema.common.js +1 -1
  45. package/lib/xt-form-schema/xt-form-schema.umd.js +1 -1
  46. package/lib/xt-grid-box/xt-grid-box.common.js +1 -1
  47. package/lib/xt-grid-box/xt-grid-box.umd.js +1 -1
  48. package/lib/xt-grid-item/xt-grid-item.common.js +1 -1
  49. package/lib/xt-grid-item/xt-grid-item.umd.js +1 -1
  50. package/lib/xt-icon/xt-icon.common.js +1 -1
  51. package/lib/xt-icon/xt-icon.umd.js +1 -1
  52. package/lib/xt-input/xt-input.common.js +1 -1
  53. package/lib/xt-input/xt-input.umd.js +1 -1
  54. package/lib/xt-list/xt-list.common.js +457 -118
  55. package/lib/xt-list/xt-list.css +1 -1
  56. package/lib/xt-list/xt-list.umd.js +457 -118
  57. package/lib/xt-list/xt-list.umd.min.js +1 -1
  58. package/lib/xt-map/xt-map.common.js +1 -1
  59. package/lib/xt-map/xt-map.umd.js +1 -1
  60. package/lib/xt-map-provider/xt-map-provider.common.js +1 -1
  61. package/lib/xt-map-provider/xt-map-provider.umd.js +1 -1
  62. package/lib/xt-page/xt-page.common.js +1 -1
  63. package/lib/xt-page/xt-page.umd.js +1 -1
  64. package/lib/xt-progress/xt-progress.common.js +1 -1
  65. package/lib/xt-progress/xt-progress.umd.js +1 -1
  66. package/lib/xt-scroll/demo.html +10 -0
  67. package/lib/xt-scroll/index.js +1 -0
  68. package/lib/xt-scroll/xt-scroll.common.js +551 -0
  69. package/lib/xt-scroll/xt-scroll.css +1 -0
  70. package/lib/xt-scroll/xt-scroll.umd.js +561 -0
  71. package/lib/xt-scroll/xt-scroll.umd.min.js +1 -0
  72. package/lib/xt-scroll-arrow/xt-scroll-arrow.common.js +591 -170
  73. package/lib/xt-scroll-arrow/xt-scroll-arrow.css +1 -1
  74. package/lib/xt-scroll-arrow/xt-scroll-arrow.umd.js +591 -170
  75. package/lib/xt-scroll-arrow/xt-scroll-arrow.umd.min.js +1 -1
  76. package/lib/xt-select-tree/xt-select-tree.common.js +1 -1
  77. package/lib/xt-select-tree/xt-select-tree.umd.js +1 -1
  78. package/lib/xt-step-price/xt-step-price.common.js +2 -2
  79. package/lib/xt-step-price/xt-step-price.umd.js +2 -2
  80. package/lib/xt-step-price-item/xt-step-price-item.common.js +1 -1
  81. package/lib/xt-step-price-item/xt-step-price-item.umd.js +1 -1
  82. package/lib/xt-tab-pane/xt-tab-pane.common.js +1 -1
  83. package/lib/xt-tab-pane/xt-tab-pane.umd.js +1 -1
  84. package/lib/xt-table/xt-table.common.js +2 -2
  85. package/lib/xt-table/xt-table.umd.js +2 -2
  86. package/lib/xt-tabs/xt-tabs.common.js +1 -1
  87. package/lib/xt-tabs/xt-tabs.umd.js +1 -1
  88. package/lib/xt-text/xt-text.common.js +1 -1
  89. package/lib/xt-text/xt-text.umd.js +1 -1
  90. package/lib/xt-time/xt-time.common.js +1 -1
  91. package/lib/xt-time/xt-time.umd.js +1 -1
  92. package/lib/xt-transfer-tree/xt-transfer-tree.common.js +1 -1
  93. package/lib/xt-transfer-tree/xt-transfer-tree.umd.js +1 -1
  94. package/lib/xt-upload/xt-upload.common.js +2 -2
  95. package/lib/xt-upload/xt-upload.umd.js +2 -2
  96. package/package.json +5 -3
  97. package/src/components/index.scss +3 -0
  98. package/src/components/theme/element-overrides.css +607 -0
  99. package/src/components/theme/element-vars.css +280 -0
  100. package/src/components/theme/iframe-injector.js +502 -0
  101. package/src/components/theme/index.js +64 -0
  102. package/src/components/xt-config-provider/style/index.scss +0 -11
  103. package/src/components/xt-date-picker/component/RangeDate.vue +1 -29
  104. package/src/components/xt-date-picker/style/index.scss +80 -53
  105. package/src/components/xt-list/index.vue +23 -12
  106. package/src/components/xt-scroll/index.js +9 -0
  107. package/src/components/xt-scroll/index.vue +363 -0
  108. package/src/components/xt-scroll/style/index.scss +107 -0
  109. package/src/components/xt-scroll-arrow/index.vue +200 -83
  110. package/src/components/xt-scroll-arrow/style/index.scss +16 -5
  111. package/src/index.js +9 -2
  112. package/src/utils/color.js +105 -0
  113. package/src/utils/index.js +456 -468
  114. package/src/utils/theme-vars.js +134 -0
  115. package/lib/css/1.87a493be.css +0 -1
  116. package/lib/xt-date-picker/css/1.87a493be.css +0 -1
@@ -1,62 +1,89 @@
1
1
 
2
2
  @import '../../../styles/variables.scss';
3
3
  .xt-date-picker-container {
4
- display: inline-block;
5
- width: 100%;
6
- --xt-input-height: 32px;
7
- }
8
- .xt-date .el-date-editor .el-input__inner {
9
- background-color: var(--xt-fill-color-blank);
10
- border-color: var(--xt-border-color);
11
- color: var(--xt-text-color-regular);
12
- font-size: var(--xt-font-size-base, 14px);
13
- border-radius: var(--xt-border-radius-small, 4px);
14
- height: var(--xt-input-height);
15
- line-height: var(--xt-input-height);
16
- padding: 0 30px;
17
- }
18
- .xt-date-dimension {
19
- margin-bottom: 8px;
20
- }
4
+ display: inline-block;
5
+ width: 100%;
6
+ --xt-input-height: 32px;
7
+ }
8
+ .xt-date .el-date-editor .el-input__inner {
9
+ background-color: var(--xt-fill-color-blank);
10
+ border-color: var(--xt-border-color);
11
+ color: var(--xt-text-color-regular);
12
+ font-size: var(--xt-font-size-base, 14px);
13
+ border-radius: var(--xt-border-radius-small, 4px);
14
+ height: var(--xt-input-height);
15
+ line-height: var(--xt-input-height);
16
+ padding: 0 30px;
17
+ }
18
+ .xt-date-dimension {
19
+ margin-bottom: 8px;
20
+ }
21
+
22
+ .xt-date-picker-wrapper {
23
+ display: inline-block;
24
+ }
21
25
 
22
- .xt-date-picker-wrapper {
23
- display: inline-block;
26
+
27
+
28
+
29
+ .quarter-panel {
30
+ padding: 10px;
31
+
32
+ .quarter-header {
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ margin-bottom: 10px;
24
37
  }
25
-
26
38
 
39
+ .quarter-list {
40
+ display: grid;
41
+ grid-template-columns: repeat(2, 1fr);
42
+ gap: 8px;
43
+ }
27
44
 
45
+ .quarter-item {
46
+ text-align: center;
47
+ padding: 8px 0;
48
+ border: 1px solid #e4e7ed;
49
+ border-radius: 4px;
50
+ cursor: pointer;
51
+ }
28
52
 
29
- .quarter-panel {
30
- padding: 10px;
31
-
32
- .quarter-header {
33
- display: flex;
34
- align-items: center;
35
- justify-content: space-between;
36
- margin-bottom: 10px;
37
- }
38
-
39
- .quarter-list {
40
- display: grid;
41
- grid-template-columns: repeat(2, 1fr);
42
- gap: 8px;
43
- }
44
-
45
- .quarter-item {
46
- text-align: center;
47
- padding: 8px 0;
48
- border: 1px solid #e4e7ed;
49
- border-radius: 4px;
50
- cursor: pointer;
51
- }
52
-
53
- .quarter-item.active {
54
- background: #409eff;
55
- color: #fff;
56
- border-color: #409eff;
57
- }
53
+ .quarter-item.active {
54
+ background: #409eff;
55
+ color: #fff;
56
+ border-color: #409eff;
57
+ }
58
+
59
+ ::v-deep .base-date-popper {
60
+ border-radius: 4px;
61
+ }
62
+ }
58
63
 
59
- ::v-deep .base-date-popper {
60
- border-radius: 4px;
61
- }
62
- }
64
+ .range-picker {
65
+ display: flex;
66
+ align-items: center;
67
+ flex-wrap: wrap;
68
+ gap: 8px;
69
+ width: 100%;
70
+ }
71
+ .range-picker .el-date-editor {
72
+ flex: 1;
73
+ min-width: 130px;
74
+ }
75
+ .split {
76
+ flex-shrink: 0;
77
+ padding: 0 4px;
78
+ }
79
+ /* 手机/窄侧边栏自动竖向排列 */
80
+ @media (max-width: 360px) {
81
+ .range-picker {
82
+ flex-direction: column;
83
+ align-items: stretch;
84
+ }
85
+ .split {
86
+ text-align: center;
87
+ margin: 4px 0;
88
+ }
89
+ }
@@ -30,10 +30,10 @@
30
30
  </div>
31
31
  </div>
32
32
 
33
- <!-- 主体区域 -->
34
- <div
33
+ <!-- 主体区域:使用 xt-scroll 作为滚动容器 -->
34
+ <xt-scroll
35
35
  ref="scrollContainer"
36
- class="xt-list__body"
36
+ :class="['xt-list__body', { 'xt-list__body--virtual': virtualScroll }]"
37
37
  :style="bodyStyle"
38
38
  @scroll="onScroll"
39
39
  >
@@ -184,7 +184,7 @@
184
184
  {{ loadMoreText }}
185
185
  </el-button>
186
186
  </div>
187
- </div>
187
+ </xt-scroll>
188
188
 
189
189
  <!-- 加载状态 -->
190
190
  <div v-if="loading" class="xt-list__loading">
@@ -212,12 +212,18 @@
212
212
  </template>
213
213
 
214
214
  <script>
215
+ import XtScroll from '../xt-scroll'
216
+
215
217
  const CARD_ITEM_HEIGHT = 160
216
218
  const GROUP_HEADER_HEIGHT = 44
217
219
 
218
220
  export default {
219
221
  name: 'XtList',
220
222
 
223
+ components: {
224
+ XtScroll
225
+ },
226
+
221
227
  props: {
222
228
  data: { type: Array, default: () => [] },
223
229
  // 分组配置
@@ -547,9 +553,10 @@ export default {
547
553
  return group._items.indexOf(item)
548
554
  },
549
555
 
550
- onScroll() {
556
+ onScroll(e) {
551
557
  if (!this.virtualScroll) return
552
- this.scrollTop = this.$refs.scrollContainer.scrollTop
558
+ // xt-scroll 传递的是 { scrollTop, scrollLeft } 对象
559
+ this.scrollTop = e && e.scrollTop != null ? e.scrollTop : 0
553
560
  if (this.rafId) return
554
561
  if (typeof requestAnimationFrame === 'undefined') return
555
562
  this.rafId = requestAnimationFrame(() => {
@@ -560,18 +567,22 @@ export default {
560
567
 
561
568
  updateContainerHeight() {
562
569
  if (this.$refs.scrollContainer) {
563
- this.containerHeight = this.$refs.scrollContainer.clientHeight
570
+ const scrollCmp = this.$refs.scrollContainer
571
+ const wrap = scrollCmp.getScrollContainer && scrollCmp.getScrollContainer()
572
+ this.containerHeight = wrap ? wrap.clientHeight : 0
564
573
  }
565
574
  },
566
575
 
567
576
  bindResizeObserver() {
568
577
  if (typeof ResizeObserver === 'undefined') return
569
- const container = this.$refs.scrollContainer
570
- if (!container) return
578
+ const scrollCmp = this.$refs.scrollContainer
579
+ if (!scrollCmp) return
580
+ const wrap = scrollCmp.getScrollContainer && scrollCmp.getScrollContainer()
581
+ if (!wrap) return
571
582
  this.resizeObserver = new ResizeObserver(() => {
572
583
  this.updateContainerHeight()
573
584
  })
574
- this.resizeObserver.observe(container)
585
+ this.resizeObserver.observe(wrap)
575
586
  },
576
587
 
577
588
  unbindResizeObserver() {
@@ -611,8 +622,8 @@ export default {
611
622
 
612
623
  // ========== 对外暴露方法 ==========
613
624
  scrollToTop() {
614
- if (this.$refs.scrollContainer) {
615
- this.$refs.scrollContainer.scrollTo({ top: 0, behavior: 'smooth' })
625
+ if (this.$refs.scrollContainer && this.$refs.scrollContainer.scrollToStart) {
626
+ this.$refs.scrollContainer.scrollToStart()
616
627
  }
617
628
  },
618
629
 
@@ -0,0 +1,9 @@
1
+ import './style/index.scss'
2
+ import XtScroll from './index.vue'
3
+
4
+ XtScroll.install = function (Vue) {
5
+ Vue.component(XtScroll.name, XtScroll)
6
+ }
7
+
8
+ export default XtScroll
9
+ export { XtScroll }
@@ -0,0 +1,363 @@
1
+ <template>
2
+ <div class="xt-scroll" :class="[`xt-scroll--${scrollDirection}`]" :style="containerStyle">
3
+ <!-- 原生滚动容器 -->
4
+ <div
5
+ ref="scrollWrap"
6
+ class="xt-scroll__wrap"
7
+ :class="[`xt-scroll__wrap--${scrollDirection}`]"
8
+ @scroll="onScroll"
9
+ >
10
+ <!-- 虚拟滚动模式 -->
11
+ <div v-if="vScroll" class="xt-scroll__virtual" :style="virtualWrapperStyle">
12
+ <!-- 虚拟滚动 phantom -->
13
+ <div
14
+ v-if="scrollDirection === 'vertical'"
15
+ class="xt-scroll__phantom"
16
+ :style="{ height: totalSize + 'px' }"
17
+ >
18
+ <div :style="{ transform: `translateY(${offsetStart}px)` }">
19
+ <template v-for="(item, index) in visibleData">
20
+ <div
21
+ :key="getItemKey(item, index)"
22
+ class="xt-scroll__item"
23
+ :style="itemStyle"
24
+ :class="itemClassName"
25
+ >
26
+ <slot name="item" :item="item" :index="getOriginalIndex(index)">
27
+ {{ item }}
28
+ </slot>
29
+ </div>
30
+ </template>
31
+ </div>
32
+ </div>
33
+
34
+ <!-- 横向虚拟滚动 phantom -->
35
+ <div
36
+ v-else-if="scrollDirection === 'horizontal'"
37
+ class="xt-scroll__phantom"
38
+ :style="{ width: totalSize + 'px' }"
39
+ >
40
+ <div :style="{ transform: `translateX(${offsetStart}px)` }">
41
+ <template v-for="(item, index) in visibleData">
42
+ <div
43
+ :key="getItemKey(item, index)"
44
+ class="xt-scroll__item"
45
+ :style="itemStyle"
46
+ :class="itemClassName"
47
+ >
48
+ <slot name="item" :item="item" :index="getOriginalIndex(index)">
49
+ {{ item }}
50
+ </slot>
51
+ </div>
52
+ </template>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <!-- 空状态 -->
58
+ <div v-if="vScroll && (!data || !data.length) && !loading" class="xt-scroll__empty">
59
+ <slot name="empty">
60
+ <span>{{ emptyText }}</span>
61
+ </slot>
62
+ </div>
63
+
64
+ <!-- 加载状态 -->
65
+ <div v-if="loading" class="xt-scroll__loading">
66
+ <slot name="loading">
67
+ <i class="el-icon-loading"></i>
68
+ <span>加载中...</span>
69
+ </slot>
70
+ </div>
71
+
72
+ <!-- 非虚拟滚动模式:默认插槽 -->
73
+ <div v-if="!vScroll" class="xt-scroll__default">
74
+ <slot></slot>
75
+ </div>
76
+
77
+ <!-- 触底/触顶加载更多 -->
78
+ <div v-if="vScroll && showLoadMore && !loading" class="xt-scroll__loadmore" @click="onLoadMore">
79
+ <el-button type="text" :loading="loadMoreLoading">
80
+ {{ loadMoreText }}
81
+ </el-button>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </template>
86
+
87
+ <script>
88
+ export default {
89
+ name: 'XtScroll',
90
+
91
+ props: {
92
+ // ========== 虚拟滚动配置 ==========
93
+ /** 是否启用虚拟滚动 */
94
+ vScroll: { type: Boolean, default: false },
95
+ /** 数据源 */
96
+ data: { type: Array, default: () => [] },
97
+ /** 每个 item 的固定尺寸(px),纵向为高度,横向为宽度 */
98
+ itemSize: { type: Number, default: 50 },
99
+ /** item 的唯一键字段名 */
100
+ keyField: { type: String, default: 'id' },
101
+ /** 预渲染缓冲区大小(item 数量) */
102
+ bufferSize: { type: Number, default: 5 },
103
+ /** 虚拟滚动方向 */
104
+ scrollDirection: {
105
+ type: String,
106
+ default: 'vertical',
107
+ validator: (v) => ['vertical', 'horizontal'].includes(v)
108
+ },
109
+ /** item 的自定义 CSS 类名 */
110
+ itemClassName: { type: String, default: '' },
111
+
112
+ // ========== 容器尺寸 ==========
113
+ height: { type: [Number, String], default: '' },
114
+ maxHeight: { type: [Number, String], default: '' },
115
+ width: { type: [Number, String], default: '' },
116
+ maxWidth: { type: [Number, String], default: '' },
117
+
118
+ // ========== 状态 ==========
119
+ loading: { type: Boolean, default: false },
120
+ emptyText: { type: String, default: '暂无数据' },
121
+
122
+ // ========== 加载更多 ==========
123
+ loadMore: { type: Boolean, default: false },
124
+ loadMoreText: { type: String, default: '加载更多' },
125
+ loadMoreLoading: { type: Boolean, default: false }
126
+ },
127
+
128
+ data() {
129
+ return {
130
+ scrollPos: 0,
131
+ containerSize: 0,
132
+ resizeObserver: null,
133
+ rafId: null
134
+ }
135
+ },
136
+
137
+ computed: {
138
+ containerStyle() {
139
+ const style = {}
140
+ if (this.height) {
141
+ style.height = typeof this.height === 'number' ? this.height + 'px' : this.height
142
+ }
143
+ if (this.maxHeight) {
144
+ style.maxHeight = typeof this.maxHeight === 'number' ? this.maxHeight + 'px' : this.maxHeight
145
+ }
146
+ if (this.width) {
147
+ style.width = typeof this.width === 'number' ? this.width + 'px' : this.width
148
+ }
149
+ if (this.maxWidth) {
150
+ style.maxWidth = typeof this.maxWidth === 'number' ? this.maxWidth + 'px' : this.maxWidth
151
+ }
152
+ return style
153
+ },
154
+
155
+ virtualWrapperStyle() {
156
+ const style = {}
157
+ if (this.scrollDirection === 'horizontal') {
158
+ style.height = '100%'
159
+ }
160
+ return style
161
+ },
162
+
163
+ itemStyle() {
164
+ return this.scrollDirection === 'vertical'
165
+ ? { height: this.itemSize + 'px' }
166
+ : { width: this.itemSize + 'px', display: 'inline-block' }
167
+ },
168
+
169
+ /** 数据总条数 */
170
+ dataCount() {
171
+ return (this.data && this.data.length) || 0
172
+ },
173
+
174
+ /** 总滚动尺寸 */
175
+ totalSize() {
176
+ return this.dataCount * this.itemSize
177
+ },
178
+
179
+ /** 虚拟滚动:可见范围起始索引 */
180
+ visibleStartIndex() {
181
+ if (!this.vScroll) return 0
182
+ const start = Math.floor(this.scrollPos / this.itemSize)
183
+ return Math.max(0, start - this.bufferSize)
184
+ },
185
+
186
+ /** 虚拟滚动:可见范围结束索引 */
187
+ visibleEndIndex() {
188
+ if (!this.vScroll) return this.dataCount
189
+ const visibleCount = Math.ceil(this.containerSize / this.itemSize)
190
+ const end = Math.ceil(this.scrollPos / this.itemSize) + visibleCount
191
+ return Math.min(this.dataCount, end + this.bufferSize)
192
+ },
193
+
194
+ /** 可见数据 */
195
+ visibleData() {
196
+ if (!this.vScroll || !this.data) return this.data || []
197
+ return this.data.slice(this.visibleStartIndex, this.visibleEndIndex)
198
+ },
199
+
200
+ /** 偏移量(px) */
201
+ offsetStart() {
202
+ return this.visibleStartIndex * this.itemSize
203
+ },
204
+
205
+ /** 是否显示加载更多 */
206
+ showLoadMore() {
207
+ if (!this.loadMore || !this.dataCount) return false
208
+ if (this.scrollDirection === 'vertical') {
209
+ return this.scrollPos + this.containerSize >= this.totalSize - this.itemSize * 2
210
+ }
211
+ return false
212
+ }
213
+ },
214
+
215
+ watch: {
216
+ data: {
217
+ handler() {
218
+ this.$nextTick(() => {
219
+ this.updateContainerSize()
220
+ })
221
+ }
222
+ },
223
+ vScroll(val) {
224
+ if (val) {
225
+ this.$nextTick(() => {
226
+ this.updateContainerSize()
227
+ this.bindResizeObserver()
228
+ })
229
+ }
230
+ }
231
+ },
232
+
233
+ mounted() {
234
+ this.$nextTick(() => {
235
+ this.updateContainerSize()
236
+ if (this.vScroll) {
237
+ this.bindResizeObserver()
238
+ }
239
+ })
240
+ },
241
+
242
+ beforeDestroy() {
243
+ this.unbindResizeObserver()
244
+ if (this.rafId && typeof cancelAnimationFrame !== 'undefined') {
245
+ cancelAnimationFrame(this.rafId)
246
+ this.rafId = null
247
+ }
248
+ },
249
+
250
+ methods: {
251
+ // ========== 虚拟滚动 ==========
252
+ onScroll() {
253
+ const wrap = this.$refs.scrollWrap
254
+ if (!wrap) return
255
+
256
+ const pos = this.scrollDirection === 'vertical' ? wrap.scrollTop : wrap.scrollLeft
257
+ this.scrollPos = pos
258
+
259
+ // 对外 emit scroll 事件,传递 scrollTop/scrollLeft 方便父组件使用
260
+ this.$emit('scroll', {
261
+ scrollTop: wrap.scrollTop,
262
+ scrollLeft: wrap.scrollLeft
263
+ })
264
+
265
+ if (this.rafId) return
266
+ if (typeof requestAnimationFrame === 'undefined') return
267
+ this.rafId = requestAnimationFrame(() => {
268
+ this.$forceUpdate()
269
+ this.rafId = null
270
+ })
271
+ },
272
+
273
+ getItemKey(item, index) {
274
+ if (item && typeof item === 'object' && this.keyField && item[this.keyField] != null) {
275
+ return item[this.keyField]
276
+ }
277
+ return `vitem_${this.visibleStartIndex + index}`
278
+ },
279
+
280
+ getOriginalIndex(index) {
281
+ return this.visibleStartIndex + index
282
+ },
283
+
284
+ // ========== 容器尺寸 ==========
285
+ updateContainerSize() {
286
+ const wrap = this.$refs.scrollWrap
287
+ if (!wrap) return
288
+
289
+ this.containerSize = this.scrollDirection === 'vertical'
290
+ ? wrap.clientHeight
291
+ : wrap.clientWidth
292
+ },
293
+
294
+ bindResizeObserver() {
295
+ if (typeof ResizeObserver === 'undefined') return
296
+ this.unbindResizeObserver()
297
+
298
+ const wrap = this.$refs.scrollWrap
299
+ if (!wrap) return
300
+
301
+ this.resizeObserver = new ResizeObserver(() => {
302
+ this.updateContainerSize()
303
+ })
304
+ this.resizeObserver.observe(wrap)
305
+ },
306
+
307
+ unbindResizeObserver() {
308
+ if (this.resizeObserver) {
309
+ this.resizeObserver.disconnect()
310
+ this.resizeObserver = null
311
+ }
312
+ },
313
+
314
+ // ========== 加载更多 ==========
315
+ onLoadMore() {
316
+ this.$emit('load-more')
317
+ },
318
+
319
+ // ========== 对外暴露方法 ==========
320
+ /** 获取原生滚动容器 DOM 元素 */
321
+ getScrollContainer() {
322
+ return this.$refs.scrollWrap
323
+ },
324
+
325
+ /** 滚动到指定位置 */
326
+ scrollTo(pos, behavior) {
327
+ const wrap = this.$refs.scrollWrap
328
+ if (!wrap) return
329
+
330
+ const opts = behavior && typeof behavior === 'string'
331
+ ? { behavior }
332
+ : { behavior: 'smooth' }
333
+
334
+ if (this.scrollDirection === 'vertical') {
335
+ wrap.scrollTo({ top: pos, ...opts })
336
+ } else {
337
+ wrap.scrollTo({ left: pos, ...opts })
338
+ }
339
+ },
340
+
341
+ /** 滚动到指定索引的 item */
342
+ scrollToItem(index) {
343
+ const pos = index * this.itemSize
344
+ this.scrollTo(pos)
345
+ },
346
+
347
+ /** 滚动到顶部/左侧 */
348
+ scrollToStart() {
349
+ this.scrollTo(0)
350
+ },
351
+
352
+ /** 滚动到底部/右侧 */
353
+ scrollToEnd() {
354
+ this.scrollTo(this.totalSize)
355
+ },
356
+
357
+ /** 获取当前滚动位置 */
358
+ getScrollPos() {
359
+ return this.scrollPos
360
+ }
361
+ }
362
+ }
363
+ </script>
@@ -0,0 +1,107 @@
1
+ @import '../../../styles/variables.scss';
2
+
3
+ .xt-scroll {
4
+ width: 100%;
5
+ height: 100%;
6
+
7
+ // 原生滚动容器
8
+ &__wrap {
9
+ width: 100%;
10
+ height: 100%;
11
+ overflow: auto;
12
+
13
+ // 纵向滚动
14
+ &--vertical {
15
+ overflow-x: hidden;
16
+ overflow-y: auto;
17
+ }
18
+
19
+ // 横向滚动
20
+ &--horizontal {
21
+ overflow-x: auto;
22
+ overflow-y: hidden;
23
+ }
24
+ }
25
+
26
+ // 虚拟滚动容器
27
+ &__virtual {
28
+ width: 100%;
29
+ position: relative;
30
+ }
31
+
32
+ &__phantom {
33
+ position: relative;
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ &__item {
38
+ box-sizing: border-box;
39
+ overflow: hidden;
40
+ }
41
+
42
+ // 默认内容区
43
+ &__default {
44
+ width: 100%;
45
+ }
46
+
47
+ // 空状态
48
+ &__empty {
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ padding: 48px 0;
53
+ color: #909399;
54
+ font-size: 14px;
55
+ }
56
+
57
+ // 加载状态
58
+ &__loading {
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ gap: 8px;
63
+ padding: 48px 0;
64
+ color: #909399;
65
+ font-size: 14px;
66
+
67
+ .el-icon-loading {
68
+ font-size: 20px;
69
+ animation: xt-scroll-rotating 2s linear infinite;
70
+ }
71
+ }
72
+
73
+ // 加载更多
74
+ &__loadmore {
75
+ display: flex;
76
+ justify-content: center;
77
+ padding: 16px 0;
78
+ }
79
+
80
+ // 横向模式
81
+ &--horizontal {
82
+ .xt-scroll__virtual {
83
+ height: 100%;
84
+ }
85
+
86
+ .xt-scroll__phantom {
87
+ white-space: nowrap;
88
+ }
89
+
90
+ .xt-scroll__item {
91
+ display: inline-block;
92
+ vertical-align: top;
93
+ }
94
+ }
95
+
96
+ // 纵向模式
97
+ &--vertical {
98
+ .xt-scroll__item {
99
+ width: 100%;
100
+ }
101
+ }
102
+ }
103
+
104
+ @keyframes xt-scroll-rotating {
105
+ from { transform: rotate(0deg); }
106
+ to { transform: rotate(360deg); }
107
+ }