@travel-screen/l-module-components 1.0.0

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 (75) hide show
  1. package/README.md +86 -0
  2. package/package.json +34 -0
  3. package/src/index.js +60 -0
  4. package/src/l-bar-chart/README.md +302 -0
  5. package/src/l-bar-chart/index.vue +405 -0
  6. package/src/l-bar-chart/interface/index.js +210 -0
  7. package/src/l-data-panel/README.md +331 -0
  8. package/src/l-data-panel/img/images.js +47 -0
  9. package/src/l-data-panel/img/l-data-panel_bg_1.png +0 -0
  10. package/src/l-data-panel/img/l-data-panel_bg_2.png +0 -0
  11. package/src/l-data-panel/img/l-data-panel_bg_3.png +0 -0
  12. package/src/l-data-panel/index.vue +150 -0
  13. package/src/l-data-panel/interface/index.js +249 -0
  14. package/src/l-data-ring/README.md +292 -0
  15. package/src/l-data-ring/img/images.js +21 -0
  16. package/src/l-data-ring/img/l-data-ring_bg.png +0 -0
  17. package/src/l-data-ring/index.vue +93 -0
  18. package/src/l-data-ring/interface/index.js +116 -0
  19. package/src/l-date-picker/README.md +276 -0
  20. package/src/l-date-picker/index.vue +185 -0
  21. package/src/l-date-picker/interface/index.js +128 -0
  22. package/src/l-icon-label/README.md +356 -0
  23. package/src/l-icon-label/img/images.js +28 -0
  24. package/src/l-icon-label/img/l-icon-label_bg.png +0 -0
  25. package/src/l-icon-label/img/l-icon-label_icon.png +0 -0
  26. package/src/l-icon-label/index.vue +162 -0
  27. package/src/l-icon-label/interface/index.js +234 -0
  28. package/src/l-label-value/README.md +358 -0
  29. package/src/l-label-value/img/images.js +19 -0
  30. package/src/l-label-value/img/l-label-value_line.png +0 -0
  31. package/src/l-label-value/index.vue +132 -0
  32. package/src/l-label-value/interface/index.js +116 -0
  33. package/src/l-metric-tile/README.md +391 -0
  34. package/src/l-metric-tile/img/images.js +19 -0
  35. package/src/l-metric-tile/img/l-metric-tile_bg_1.png +0 -0
  36. package/src/l-metric-tile/index.vue +138 -0
  37. package/src/l-metric-tile/interface/index.js +241 -0
  38. package/src/l-module-box/README.md +289 -0
  39. package/src/l-module-box/img/images.js +28 -0
  40. package/src/l-module-box/img/l-module-box_bg.png +0 -0
  41. package/src/l-module-box/img/l-module-box_icon.png +0 -0
  42. package/src/l-module-box/index.vue +125 -0
  43. package/src/l-module-box/interface/index.js +93 -0
  44. package/src/l-module-tag/README.md +243 -0
  45. package/src/l-module-tag/index.vue +137 -0
  46. package/src/l-module-tag/interface/index.js +91 -0
  47. package/src/l-rank-list/README.md +553 -0
  48. package/src/l-rank-list/img/images.js +34 -0
  49. package/src/l-rank-list/img/l-rank-list_icon_line.png +0 -0
  50. package/src/l-rank-list/img/l-rank-list_icon_rank_1.png +0 -0
  51. package/src/l-rank-list/img/l-rank-list_icon_rank_2.png +0 -0
  52. package/src/l-rank-list/img/l-rank-list_icon_rank_other.png +0 -0
  53. package/src/l-rank-list/index.vue +610 -0
  54. package/src/l-rank-list/interface/index.js +343 -0
  55. package/src/l-stat-card/README.md +309 -0
  56. package/src/l-stat-card/img/images.js +36 -0
  57. package/src/l-stat-card/img/img-bg.png +0 -0
  58. package/src/l-stat-card/img/l-stat-card_img_1.png +0 -0
  59. package/src/l-stat-card/index.vue +136 -0
  60. package/src/l-stat-card/interface/index.js +159 -0
  61. package/src/l-tabs/README.md +285 -0
  62. package/src/l-tabs/img/images.js +19 -0
  63. package/src/l-tabs/img/l-tabs_bg.png +0 -0
  64. package/src/l-tabs/index.vue +97 -0
  65. package/src/l-tabs/interface/index.js +115 -0
  66. package/src/l-trend-card/README.md +415 -0
  67. package/src/l-trend-card/img/images.js +41 -0
  68. package/src/l-trend-card/img/l-trend-card_bg.png +0 -0
  69. package/src/l-trend-card/img/l-trend-card_icon_1.png +0 -0
  70. package/src/l-trend-card/img/l-trend-card_icon_down.png +0 -0
  71. package/src/l-trend-card/img/l-trend-card_icon_up.png +0 -0
  72. package/src/l-trend-card/index.vue +181 -0
  73. package/src/l-trend-card/interface/index.js +243 -0
  74. package/src/utils/tooltip.js +30 -0
  75. package/src/utils/util.js +27 -0
@@ -0,0 +1,610 @@
1
+ <template>
2
+ <div
3
+ class="l-rank-list"
4
+ :class="{
5
+ 'l-rank-list--manual-scroll': !autoScroll,
6
+ 'l-rank-list--auto-scroll': autoScroll
7
+ }"
8
+ :style="containerStyle"
9
+ ref="rankListContainer"
10
+ @mouseenter="handleMouseEnter"
11
+ @mouseleave="handleMouseLeave"
12
+ @wheel="handleWheel"
13
+ >
14
+ <div v-if="autoScroll && shouldScroll" class="l-rank-list__scroll-wrapper" :style="scrollWrapperStyle" ref="scrollWrapper">
15
+ <!-- 自动滚动模式 -->
16
+ <div v-for="(item, index) in scrollDataList" :key="'scroll_' + index" class="l-rank-list__item" :style="listItemStyle">
17
+ <div :class="['l-rank-list__rank-icon', `l-rank-list__rank-icon--${getRankClass(index + 1)}`]" :style="getRankIconStyle(getRealRank(index))">
18
+ {{ getRealRank(index) }}
19
+ </div>
20
+ <div class="l-rank-list__right">
21
+ <!-- 布局类型:value-top(数值在标题右侧) -->
22
+ <template v-if="layoutType === 'value-top'">
23
+ <div class="l-rank-list__title" :style="titleStyle">
24
+ <span>{{ item.name }}</span>
25
+ <div class="l-rank-list__value" :style="topValueStyle">{{ item.value }}<span :style="unitStyle">{{ unit }}</span></div>
26
+ </div>
27
+ <div class="l-rank-list__bottom" :style="bottomStyle">
28
+ <div class="l-rank-list__progress" :style="progressStyle">
29
+ <div
30
+ :style="{
31
+ background: progressBarBg,
32
+ boxShadow: progressBarBoxShadow,
33
+ borderRadius: typeof progressBarBorderRadius === 'number' ? `${progressBarBorderRadius}px` : progressBarBorderRadius
34
+ }"
35
+ class="l-rank-list__progress-bar">
36
+ <div
37
+ :style="{
38
+ width: item.value / maxValue * 100 + '%',
39
+ background: (getRealRank(index) - 1) > 1 ? progressBarActiveBg : getRankColor(getRealRank(index) - 1),
40
+ borderRadius: typeof progressBarBorderRadius === 'number' ? `${progressBarBorderRadius}px` : progressBarBorderRadius
41
+ }"
42
+ class="l-rank-list__progress-bar-active"></div>
43
+ <img
44
+ :style="{
45
+ position: 'absolute',
46
+ left: `calc(${item.value / maxValue * 100}% - ${sliderIconWidth / 2}px)`,
47
+ width: typeof sliderIconWidth === 'number' ? `${sliderIconWidth}px` : sliderIconWidth,
48
+ height: typeof sliderIconHeight === 'number' ? `${sliderIconHeight}px` : sliderIconHeight,
49
+ top: typeof sliderIconTop === 'number' ? `${sliderIconTop}px` : sliderIconTop
50
+ }"
51
+ :src="computedSliderIcon"
52
+ alt=""
53
+ />
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <!-- 布局类型:value-bottom(数值在进度条右侧,默认) -->
60
+ <template v-else>
61
+ <div class="l-rank-list__title" :style="titleStyle">
62
+ <span>{{ item.name }}</span>
63
+ </div>
64
+ <div class="l-rank-list__bottom" :style="bottomStyle">
65
+ <div class="l-rank-list__progress" :style="progressStyle">
66
+ <div
67
+ :style="{
68
+ background: progressBarBg,
69
+ boxShadow: progressBarBoxShadow,
70
+ borderRadius: typeof progressBarBorderRadius === 'number' ? `${progressBarBorderRadius}px` : progressBarBorderRadius
71
+ }"
72
+ class="l-rank-list__progress-bar">
73
+ <div
74
+ :style="{
75
+ width: item.value / maxValue * 100 + '%',
76
+ background: (getRealRank(index) - 1) > 1 ? progressBarActiveBg : getRankColor(getRealRank(index) - 1),
77
+ borderRadius: typeof progressBarBorderRadius === 'number' ? `${progressBarBorderRadius}px` : progressBarBorderRadius
78
+ }"
79
+ class="l-rank-list__progress-bar-active"></div>
80
+ <img
81
+ :style="{
82
+ position: 'absolute',
83
+ left: `calc(${item.value / maxValue * 100}% - ${sliderIconWidth / 2}px)`,
84
+ width: typeof sliderIconWidth === 'number' ? `${sliderIconWidth}px` : sliderIconWidth,
85
+ height: typeof sliderIconHeight === 'number' ? `${sliderIconHeight}px` : sliderIconHeight,
86
+ top: typeof sliderIconTop === 'number' ? `${sliderIconTop}px` : sliderIconTop
87
+ }"
88
+ :src="computedSliderIcon"
89
+ alt=""
90
+ />
91
+ </div>
92
+ </div>
93
+ <div class="l-rank-list__value" :style="[{...valueStyle}, {...valueContainerStyle}]">{{ item.value }}<span :style="unitStyle">{{ unit }}</span></div>
94
+ </div>
95
+ </template>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ <template v-else>
100
+ <!-- 非自动滚动模式:显示所有数据,支持原生滚动 -->
101
+ <div v-for="(item, index) in dataList" :key="'line_' + index" class="l-rank-list__item" :style="listItemStyle">
102
+ <div :class="['l-rank-list__rank-icon', `l-rank-list__rank-icon--${getRankClass(index + 1)}`]" :style="getRankIconStyle(index + 1)">
103
+ {{ index + 1 }}
104
+ </div>
105
+ <div class="l-rank-list__right">
106
+ <!-- 布局类型:value-top(数值在标题右侧) -->
107
+ <template v-if="layoutType === 'value-top'">
108
+ <div class="l-rank-list__title" :style="titleStyle">
109
+ <span>{{ item.name }}</span>
110
+ <div class="l-rank-list__value" :style="topValueStyle">{{ item.value }}<span :style="unitStyle">{{ unit }}</span></div>
111
+ </div>
112
+ <div class="l-rank-list__bottom" :style="bottomStyle">
113
+ <div class="l-rank-list__progress" :style="progressStyle">
114
+ <div
115
+ :style="{
116
+ background: progressBarBg,
117
+ boxShadow: progressBarBoxShadow,
118
+ borderRadius: typeof progressBarBorderRadius === 'number' ? `${progressBarBorderRadius}px` : progressBarBorderRadius
119
+ }"
120
+ class="l-rank-list__progress-bar">
121
+ <div
122
+ :style="{
123
+ width: item.value / maxValue * 100 + '%',
124
+ background: index > 1 ? progressBarActiveBg : getRankColor(index),
125
+ borderRadius: typeof progressBarBorderRadius === 'number' ? `${progressBarBorderRadius}px` : progressBarBorderRadius
126
+ }"
127
+ class="l-rank-list__progress-bar-active"></div>
128
+ <img
129
+ :style="{
130
+ position: 'absolute',
131
+ left: `calc(${item.value / maxValue * 100}% - ${sliderIconWidth / 2}px)`,
132
+ width: typeof sliderIconWidth === 'number' ? `${sliderIconWidth}px` : sliderIconWidth,
133
+ height: typeof sliderIconHeight === 'number' ? `${sliderIconHeight}px` : sliderIconHeight,
134
+ top: typeof sliderIconTop === 'number' ? `${sliderIconTop}px` : sliderIconTop
135
+ }"
136
+ :src="computedSliderIcon"
137
+ alt=""
138
+ />
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </template>
143
+
144
+ <!-- 布局类型:value-bottom(数值在进度条右侧,默认) -->
145
+ <template v-else>
146
+ <div class="l-rank-list__title" :style="titleStyle">
147
+ <span>{{ item.name }}</span>
148
+ </div>
149
+ <div class="l-rank-list__bottom" :style="bottomStyle">
150
+ <div class="l-rank-list__progress" :style="progressStyle">
151
+ <div
152
+ :style="{
153
+ background: progressBarBg,
154
+ boxShadow: progressBarBoxShadow,
155
+ borderRadius: typeof progressBarBorderRadius === 'number' ? `${progressBarBorderRadius}px` : progressBarBorderRadius
156
+ }"
157
+ class="l-rank-list__progress-bar">
158
+ <div
159
+ :style="{
160
+ width: item.value / maxValue * 100 + '%',
161
+ background: index > 1 ? progressBarActiveBg : getRankColor(index),
162
+ borderRadius: typeof progressBarBorderRadius === 'number' ? `${progressBarBorderRadius}px` : progressBarBorderRadius
163
+ }"
164
+ class="l-rank-list__progress-bar-active"></div>
165
+ <img
166
+ :style="{
167
+ position: 'absolute',
168
+ left: `calc(${item.value / maxValue * 100}% - ${sliderIconWidth / 2}px)`,
169
+ width: typeof sliderIconWidth === 'number' ? `${sliderIconWidth}px` : sliderIconWidth,
170
+ height: typeof sliderIconHeight === 'number' ? `${sliderIconHeight}px` : sliderIconHeight,
171
+ top: typeof sliderIconTop === 'number' ? `${sliderIconTop}px` : sliderIconTop
172
+ }"
173
+ :src="computedSliderIcon"
174
+ alt=""
175
+ />
176
+ </div>
177
+ </div>
178
+ <div class="l-rank-list__value" :style="[{...valueStyle}, {...valueContainerStyle}]">{{ item.value }}<span :style="unitStyle">{{ unit }}</span></div>
179
+ </div>
180
+ </template>
181
+ </div>
182
+ </div>
183
+ </template>
184
+ </div>
185
+ </template>
186
+
187
+ <script>
188
+ import interfaceObj from './interface/index'
189
+ import { getLineIconPath, getRankIconPath } from './img/images'
190
+
191
+ export default {
192
+ name: 'LRankList',
193
+ props: interfaceObj,
194
+ components: {},
195
+ data() {
196
+ return {
197
+ maxValue: 0,
198
+ scrollPosition: 0,
199
+ animationFrameId: null,
200
+ isMouseInside: false, // 鼠标是否在容器内
201
+ manualScrollOffset: 0 // 手动滚动的偏移量
202
+ }
203
+ },
204
+ computed: {
205
+ // 是否应该滚动(数据量超过显示限制且开启了自动滚动)
206
+ shouldScroll() {
207
+ return this.dataList.length > this.displayLimit
208
+ },
209
+
210
+ // 滚动数据列表(复制一份数据以实现无缝滚动)
211
+ scrollDataList() {
212
+ if (!this.shouldScroll) return this.dataList.slice(0, this.displayLimit)
213
+ // 复制数据以实现无缝滚动
214
+ return [...this.dataList, ...this.dataList.slice(0, this.displayLimit)]
215
+ },
216
+
217
+ // 获取真实排名(处理复制数据后的排名)
218
+ getRealRank() {
219
+ return (index) => {
220
+ if (index < this.dataList.length) {
221
+ return index + 1
222
+ }
223
+ return index - this.dataList.length + 1
224
+ }
225
+ },
226
+
227
+ // 滚动容器样式
228
+ scrollWrapperStyle() {
229
+ // 自动滚动模式下,根据鼠标位置和手动滚动偏移计算最终位置
230
+ const finalPosition = this.isMouseInside
231
+ ? this.scrollPosition + this.manualScrollOffset
232
+ : this.scrollPosition
233
+
234
+ return {
235
+ transform: `translateY(-${finalPosition}px)`,
236
+ transition: 'none'
237
+ }
238
+ },
239
+
240
+ // 滑块图标(外部传入优先,否则使用默认)
241
+ computedSliderIcon() {
242
+ if (this.sliderIcon) {
243
+ return this.sliderIcon
244
+ }
245
+ return getLineIconPath()
246
+ },
247
+
248
+ // 容器样式
249
+ containerStyle() {
250
+ return {
251
+ width: this.containerWidth,
252
+ height: this.containerHeight,
253
+ left: typeof this.containerLeft === 'number' ? `${this.containerLeft}px` : this.containerLeft
254
+ }
255
+ },
256
+
257
+ // 列表项样式
258
+ listItemStyle() {
259
+ return {
260
+ width: this.listItemWidth,
261
+ gap: typeof this.listItemGap === 'number' ? `${this.listItemGap}px` : this.listItemGap
262
+ }
263
+ },
264
+
265
+ // 排名图标样式
266
+ rankIconStyle() {
267
+ return {
268
+ width: typeof this.rankIconWidth === 'number' ? `${this.rankIconWidth}px` : this.rankIconWidth,
269
+ height: typeof this.rankIconHeight === 'number' ? `${this.rankIconHeight}px` : this.rankIconHeight,
270
+ fontSize: typeof this.rankIconFontSize === 'number' ? `${this.rankIconFontSize}px` : this.rankIconFontSize,
271
+ fontFamily: this.rankIconFontFamily,
272
+ fontWeight: this.rankIconFontWeight,
273
+ color: this.rankIconColor
274
+ }
275
+ },
276
+
277
+ // 标题样式
278
+ titleStyle() {
279
+ return {
280
+ fontFamily: this.titleFontFamily,
281
+ fontWeight: this.titleFontWeight,
282
+ fontSize: typeof this.titleFontSize === 'number' ? `${this.titleFontSize}px` : this.titleFontSize,
283
+ color: this.titleColor,
284
+ marginBottom: this.layoutType === 'value-top' ? (typeof this.titleMarginBottom === 'number' ? `${this.titleMarginBottom}px` : this.titleMarginBottom) : '0'
285
+ }
286
+ },
287
+
288
+ // 底部容器样式
289
+ bottomStyle() {
290
+ return {
291
+ gap: typeof this.listItemGap === 'number' ? `${this.listItemGap}px` : this.listItemGap
292
+ }
293
+ },
294
+
295
+ // 进度条容器样式
296
+ progressStyle() {
297
+ return {
298
+ width: typeof this.progressBarWidth === 'number' ? `${this.progressBarWidth}%` : this.progressBarWidth,
299
+ height: typeof this.progressBarHeight === 'number' ? `${this.progressBarHeight}px` : this.progressBarHeight
300
+ }
301
+ },
302
+
303
+ // 数值容器样式
304
+ valueContainerStyle() {
305
+ return {
306
+ width: typeof this.valueWidth === 'number' ? `${this.valueWidth}%` : this.valueWidth
307
+ }
308
+ },
309
+
310
+ // 数值样式
311
+ valueStyle() {
312
+ return {
313
+ minWidth: typeof this.valueMinWidth === 'number' ? `${this.valueMinWidth}px` : this.valueMinWidth,
314
+ fontFamily: this.valueFontFamily,
315
+ fontWeight: this.valueFontWeight,
316
+ fontSize: typeof this.valueFontSize === 'number' ? `${this.valueFontSize}px` : this.valueFontSize,
317
+ color: this.valueColor,
318
+ background: this.valueBackground,
319
+ WebkitBackgroundClip: this.valueBackgroundClip,
320
+ backgroundClip: this.valueBackgroundClip,
321
+ lineHeight: typeof this.valueLineHeight === 'number' ? `${this.valueLineHeight}px` : this.valueLineHeight,
322
+ fontStyle: this.valueFontStyle,
323
+ textShadow: this.valueTextShadow,
324
+ textAlign: this.valueTextAlign
325
+ }
326
+ },
327
+
328
+ // 顶部数值样式(value-top 布局专用)
329
+ topValueStyle() {
330
+ return {
331
+ fontFamily: this.valueFontFamily,
332
+ fontWeight: this.valueFontWeight,
333
+ fontSize: typeof this.topValueFontSize === 'number' ? `${this.topValueFontSize}px` : this.topValueFontSize,
334
+ color: this.topValueColor,
335
+ background: this.topValueBackground,
336
+ WebkitBackgroundClip: this.topValueBackgroundClip,
337
+ backgroundClip: this.topValueBackgroundClip,
338
+ lineHeight: typeof this.topValueLineHeight === 'number' ? `${this.topValueLineHeight}px` : this.topValueLineHeight,
339
+ fontStyle: this.topValueFontStyle,
340
+ textShadow: this.topValueTextShadow
341
+ }
342
+ },
343
+
344
+ // 单位样式
345
+ unitStyle() {
346
+ return {
347
+ fontFamily: this.unitFontFamily,
348
+ fontWeight: this.unitFontWeight,
349
+ fontSize: typeof this.unitFontSize === 'number' ? `${this.unitFontSize}px` : this.unitFontSize,
350
+ marginLeft: typeof this.unitMarginLeft === 'number' ? `${this.unitMarginLeft}px` : this.unitMarginLeft
351
+ }
352
+ }
353
+ },
354
+ watch: {
355
+ dataList: {
356
+ handler() {
357
+ this.maxValue = this.dataList.reduce((max, item) => Math.max(max, item.value), -Infinity)
358
+ this.manualScrollOffset = 0 // 数据变化时重置手动滚动偏移
359
+ },
360
+ deep: true,
361
+ immediate: true
362
+ },
363
+ // 监听 autoScroll 变化,关闭时立即停止滚动
364
+ autoScroll(newVal) {
365
+ if (!newVal) {
366
+ this.stopScroll()
367
+ this.manualScrollOffset = 0
368
+ } else if (this.shouldScroll) {
369
+ this.startScroll()
370
+ }
371
+ }
372
+ },
373
+ mounted() {
374
+ console.log('[LRankList] mounted', {
375
+ autoScroll: this.autoScroll,
376
+ shouldScroll: this.shouldScroll,
377
+ dataListLength: this.dataList.length,
378
+ displayLimit: this.displayLimit
379
+ })
380
+
381
+ if (this.autoScroll && this.shouldScroll) {
382
+ console.log('[LRankList] 启动自动滚动')
383
+ this.startScroll()
384
+ } else {
385
+ console.log('[LRankList] 不启动自动滚动')
386
+ }
387
+ },
388
+ beforeDestroy() {
389
+ this.stopScroll()
390
+ },
391
+ created() {},
392
+ methods: {
393
+ // 获取排名样式类名
394
+ getRankClass(rank) {
395
+ return `rank-${rank}`
396
+ },
397
+
398
+ // 获取排名图标路径
399
+ getRankIcon(rank) {
400
+ if (rank === 1 && this.rankIcon1) return this.rankIcon1
401
+ if (rank === 2 && this.rankIcon2) return this.rankIcon2
402
+ if (rank > 2 && this.rankIconOther) return this.rankIconOther
403
+ return getRankIconPath(rank)
404
+ },
405
+
406
+ // 获取排名图标样式
407
+ getRankIconStyle(rank) {
408
+ return {
409
+ ...this.rankIconStyle,
410
+ background: `url(${this.getRankIcon(rank)}) 0 0 no-repeat`,
411
+ backgroundSize: '100% 100%'
412
+ }
413
+ },
414
+
415
+ // 获取排名颜色
416
+ getRankColor(index) {
417
+ if (index === 0) return this.rank1Color
418
+ if (index === 1) return this.rank2Color
419
+ return this.progressBarActiveBg
420
+ },
421
+
422
+ handleClick() {},
423
+
424
+ // 鼠标进入容器
425
+ handleMouseEnter() {
426
+ this.isMouseInside = true
427
+ // 鼠标移入时停止自动滚动
428
+ if (this.animationFrameId) {
429
+ cancelAnimationFrame(this.animationFrameId)
430
+ this.animationFrameId = null
431
+ }
432
+ },
433
+
434
+ // 鼠标离开容器
435
+ handleMouseLeave() {
436
+ this.isMouseInside = false
437
+ // 鼠标离开时,重置手动滚动偏移
438
+ this.manualScrollOffset = 0
439
+ // 恢复自动滚动
440
+ if (this.autoScroll && this.shouldScroll) {
441
+ this.startScroll()
442
+ }
443
+ },
444
+
445
+ // 处理鼠标滚轮事件
446
+ handleWheel(event) {
447
+ // 自动滚动模式下才拦截滚轮事件
448
+ if (!this.autoScroll || !this.shouldScroll) return
449
+
450
+ // 阻止默认滚动行为
451
+ event.preventDefault()
452
+
453
+ // 根据滚轮方向调整手动滚动偏移
454
+ // deltaY > 0 表示向下滚动,deltaY < 0 表示向上滚动
455
+ this.manualScrollOffset += event.deltaY
456
+
457
+ // 计算单份数据的高度,用于实现无缝循环
458
+ const container = this.$refs.rankListContainer
459
+ const wrapper = this.$refs.scrollWrapper
460
+ if (!container || !wrapper) return
461
+
462
+ const itemHeight = wrapper.querySelector('.l-rank-list__item')?.clientHeight || 30
463
+ const singleListHeight = this.dataList.length * itemHeight
464
+
465
+ // 实现无缝循环滚动:允许向任意方向无限滚动
466
+ // 当向上滚动超过顶部时,循环到底部
467
+ if (this.manualScrollOffset < 0) {
468
+ this.manualScrollOffset += singleListHeight
469
+ }
470
+ // 当向下滚动超过底部时,循环到顶部
471
+ const containerHeight = container.clientHeight
472
+ const maxScroll = singleListHeight - containerHeight
473
+ if (this.manualScrollOffset > maxScroll) {
474
+ this.manualScrollOffset -= singleListHeight
475
+ }
476
+ },
477
+
478
+ // 开始自动滚动
479
+ startScroll() {
480
+ if (this.animationFrameId) return
481
+
482
+ const container = this.$refs.rankListContainer
483
+ const wrapper = this.$refs.scrollWrapper
484
+ if (!container || !wrapper) return
485
+
486
+ const itemHeight = wrapper.querySelector('.l-rank-list__item')?.clientHeight || 30
487
+
488
+ let lastTime = 0
489
+ const animate = (currentTime) => {
490
+ if (!lastTime) lastTime = currentTime
491
+ const deltaTime = (currentTime - lastTime) / 1000 // 转换为秒
492
+
493
+ this.scrollPosition += this.scrollSpeed * deltaTime
494
+
495
+ // 当滚动到复制数据的位置时,重置到开头实现无缝循环
496
+ const singleListHeight = this.dataList.length * itemHeight
497
+ if (this.scrollPosition >= singleListHeight) {
498
+ this.scrollPosition -= singleListHeight
499
+ }
500
+
501
+ lastTime = currentTime
502
+ this.animationFrameId = requestAnimationFrame(animate)
503
+ }
504
+
505
+ this.animationFrameId = requestAnimationFrame(animate)
506
+ },
507
+
508
+ // 停止自动滚动
509
+ stopScroll() {
510
+ if (this.animationFrameId) {
511
+ cancelAnimationFrame(this.animationFrameId)
512
+ this.animationFrameId = null
513
+ }
514
+ this.scrollPosition = 0
515
+ },
516
+
517
+ // 暂停滚动(鼠标悬停时调用)
518
+ pauseScroll() {
519
+ if (this.animationFrameId) {
520
+ cancelAnimationFrame(this.animationFrameId)
521
+ this.animationFrameId = null
522
+ }
523
+ },
524
+
525
+ // 恢复滚动(鼠标离开时调用)
526
+ resumeScroll() {
527
+ if (this.autoScroll && this.shouldScroll && !this.animationFrameId) {
528
+ this.startScroll()
529
+ }
530
+ }
531
+ }
532
+ }
533
+ </script>
534
+
535
+ <style lang="scss" scoped>
536
+ .l-rank-list {
537
+ overflow: hidden;
538
+ position: relative;
539
+
540
+ // 未开启自动滚动时使用原生滚动
541
+ &--manual-scroll {
542
+ overflow-y: auto;
543
+ overflow-x: hidden;
544
+ }
545
+
546
+ // 开启自动滚动时,允许鼠标滚轮事件
547
+ &--auto-scroll {
548
+ overflow: hidden;
549
+ }
550
+
551
+ &__scroll-wrapper {
552
+ will-change: transform;
553
+ }
554
+
555
+ &__item {
556
+ display: flex;
557
+ align-items: center;
558
+ }
559
+
560
+ &__rank-icon {
561
+ flex-shrink: 0;
562
+ display: flex;
563
+ align-items: center;
564
+ justify-content: center;
565
+ }
566
+
567
+ &__right {
568
+ flex: 1;
569
+ }
570
+
571
+ &__title {
572
+ font-style: normal;
573
+ display: flex;
574
+ justify-content: space-between;
575
+ }
576
+
577
+ &__bottom {
578
+ display: flex;
579
+ align-items: center;
580
+ }
581
+
582
+ &__progress {
583
+ flex: 1;
584
+ display: flex;
585
+ align-items: center;
586
+ min-width: 0; /* 防止 flex 子元素溢出 */
587
+ }
588
+
589
+ &__progress-bar {
590
+ width: 100%;
591
+ height: 100%;
592
+ position: relative;
593
+ }
594
+
595
+ &__progress-bar-active {
596
+ height: 100%;
597
+ position: absolute;
598
+ top: 0;
599
+ left: 0;
600
+ }
601
+
602
+ &__value {
603
+ flex-shrink: 0;
604
+
605
+ span {
606
+ font-style: normal;
607
+ }
608
+ }
609
+ }
610
+ </style>