hookehuyr

feat(弹幕组件): 添加原生弹幕组件并集成到排行榜页面

- 新增 NativeDanmuComponent 原生弹幕组件,支持多轨道弹幕展示
- 在家庭排行榜页面集成弹幕功能,展示助力榜内容
- 调整排行榜卡片布局和样式,优化助力码按钮位置
- 添加弹幕点击和悬停事件处理逻辑
...@@ -13,6 +13,7 @@ declare module 'vue' { ...@@ -13,6 +13,7 @@ declare module 'vue' {
13 BottomNav: typeof import('./src/components/BottomNav.vue')['default'] 13 BottomNav: typeof import('./src/components/BottomNav.vue')['default']
14 FamilyAlbum: typeof import('./src/components/FamilyAlbum.vue')['default'] 14 FamilyAlbum: typeof import('./src/components/FamilyAlbum.vue')['default']
15 GlassCard: typeof import('./src/components/GlassCard.vue')['default'] 15 GlassCard: typeof import('./src/components/GlassCard.vue')['default']
16 + NativeDanmuComponent: typeof import('./src/components/NativeDanmuComponent.vue')['default']
16 NavBar: typeof import('./src/components/navBar.vue')['default'] 17 NavBar: typeof import('./src/components/navBar.vue')['default']
17 NumberRoll: typeof import('./src/components/NumberRoll.vue')['default'] 18 NumberRoll: typeof import('./src/components/NumberRoll.vue')['default']
18 NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet'] 19 NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet']
......
1 +<template>
2 + <view class="native-danmu-container">
3 + <!-- 弹幕显示区域 -->
4 + <view
5 + class="danmu-display-area"
6 + :style="{ height: containerHeight + 'rpx' }"
7 + >
8 + <!-- 弹幕轨道 -->
9 + <view
10 + v-for="(track, trackIndex) in tracks"
11 + :key="trackIndex"
12 + class="danmu-track"
13 + :style="{ top: trackIndex * trackHeight + 'rpx', height: trackHeight + 'rpx' }"
14 + >
15 + <!-- 轨道中的弹幕 -->
16 + <view
17 + v-for="danmu in track.danmus"
18 + :key="danmu.id"
19 + class="danmu-item"
20 + :style="getDanmuStyle(danmu, trackIndex)"
21 + @tap="handleDanmuClick(danmu)"
22 + >
23 + <!-- 弹幕内容 -->
24 + <view class="danmu-content">
25 + <!-- 头像 -->
26 + <view class="danmu-avatar-container">
27 + <image
28 + :src="danmu.data.avatar"
29 + class="danmu-avatar"
30 + mode="aspectFill"
31 + />
32 + </view>
33 +
34 + <!-- 信息区域 -->
35 + <view class="danmu-info">
36 + <!-- 标题行 -->
37 + <view class="danmu-title-row">
38 + <text class="family-name">{{ danmu.data.familyName }}</text>
39 + </view>
40 +
41 + <!-- 介绍 -->
42 + <text class="family-intro">{{ danmu.data.familyIntro }}</text>
43 + </view>
44 + </view>
45 + </view>
46 + </view>
47 + </view>
48 + </view>
49 +</template>
50 +
51 +<script setup>
52 +import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
53 +import Taro from '@tarojs/taro'
54 +
55 +// Props
56 +const props = defineProps({
57 + // 弹幕容器高度
58 + containerHeight: {
59 + type: Number,
60 + default: 400
61 + },
62 + // 是否显示控制面板
63 + showControls: {
64 + type: Boolean,
65 + default: true
66 + },
67 + // 自定义弹幕数据
68 + customComments: {
69 + type: Array,
70 + default: () => []
71 + },
72 + // 弹幕速度 (rpx/s)
73 + danmuSpeed: {
74 + type: Number,
75 + default: 120 // 提高默认速度,让弹幕移动更流畅
76 + },
77 + // 轨道数量
78 + trackCount: {
79 + type: Number,
80 + default: 6 // 适当减少轨道数量,确保有足够间距
81 + }
82 +})
83 +
84 +// Emits
85 +const emit = defineEmits(['danmu-click', 'danmu-hover'])
86 +
87 +// 响应式数据
88 +const isPlaying = ref(false)
89 +const danmuId = ref(0)
90 +const trackHeight = ref(110) // 适配最多3行文字显示,确保弹幕之间有足够间距
91 +const animationTimers = ref(new Map()) // 存储动画定时器
92 +
93 +// 轨道系统
94 +const tracks = reactive(
95 + Array.from({ length: props.trackCount }, (_, index) => ({
96 + id: index,
97 + danmus: [],
98 + lastDanmuTime: 0
99 + }))
100 +)
101 +
102 +// Mock 数据 - 家庭弹幕内容
103 +const baseFamilyData = {
104 + familyName: '幸福之家',
105 + familyIntro: '7口之家,父母健康,有儿有女,孩子活泼可爱,我们工作顺利',
106 + avatar: 'https://cdn.ipadbiz.cn/lls_prog/images/%E5%85%A8%E5%AE%B6%E7%A6%8F3_%E5%89%AF%E6%9C%AC.jpg?imageMogr2/strip/quality/60',
107 +}
108 +
109 +// 生成1000条相同的弹幕数据
110 +const mockFamilyDanmus = Array.from({ length: 1000 }, (_, index) => ({
111 + id: (index + 1).toString(),
112 + ...baseFamilyData
113 +}))
114 +
115 +// 获取弹幕样式
116 +const getDanmuStyle = (danmu, trackIndex) => {
117 + // 计算垂直位置,确保每个轨道有明确的垂直间距
118 + const topPosition = trackIndex * (trackHeight.value + 15) // 轨道间增加15rpx间距,适配3行文字
119 +
120 + return {
121 + transform: `translateX(${danmu.x}rpx)`,
122 + transition: danmu.isMoving ? `transform ${danmu.duration}ms linear` : 'none',
123 + opacity: danmu.opacity || 1,
124 + // 明确设置垂直位置
125 + position: 'absolute',
126 + left: 0,
127 + top: `${topPosition}rpx`,
128 + zIndex: 10,
129 + // 添加硬件加速
130 + willChange: 'transform'
131 + }
132 +}
133 +
134 +// 检查轨道中是否有足够空间放置新弹幕
135 +const checkTrackSpace = (track, danmuWidth = 350) => {
136 + const now = Date.now()
137 +
138 + // 如果轨道为空,可以直接放置
139 + if (track.danmus.length === 0) {
140 + return true
141 + }
142 +
143 + // 检查最后一个弹幕的位置和时间
144 + const lastDanmu = track.danmus[track.danmus.length - 1]
145 + const timeSinceLastDanmu = now - track.lastDanmuTime
146 +
147 + // 更精确的位置计算
148 + const elapsedTime = now - lastDanmu.startTime
149 + const totalDistance = 750 + danmuWidth + 100 // 总移动距离
150 + const moveDistance = Math.min((elapsedTime / lastDanmu.duration) * totalDistance, totalDistance)
151 + const lastDanmuCurrentX = 750 - moveDistance
152 +
153 + // 调整安全间距,考虑3行文字的情况
154 + const minSafeDistance = danmuWidth * 0.4 // 增加到40%,为3行文字预留更多空间
155 + const safeDistance = danmuWidth + minSafeDistance // 弹幕宽度 + 40%弹幕长度作为间距
156 + const hasEnoughSpace = lastDanmuCurrentX < (750 - safeDistance)
157 +
158 + // 适当增加时间间隔,确保3行文字弹幕有足够的显示时间
159 + const minTimeInterval = 1800 // 从1600ms增加到1800ms,为3行文字预留更多时间
160 + const hasEnoughTime = timeSinceLastDanmu >= minTimeInterval
161 +
162 + // 双重检查:时间和空间都满足才允许放置
163 + return hasEnoughTime && hasEnoughSpace
164 +}
165 +
166 +// 找到可用的轨道 - 随机选择而非按顺序
167 +const findAvailableTrack = () => {
168 + // 过滤出有足够空间的轨道
169 + const availableTracks = tracks.filter(track => checkTrackSpace(track))
170 +
171 + // 如果没有可用轨道,返回null
172 + if (availableTracks.length === 0) {
173 + return null
174 + }
175 +
176 + // 从可用轨道中随机选择一个
177 + const randomIndex = Math.floor(Math.random() * availableTracks.length)
178 + return availableTracks[randomIndex]
179 +}
180 +
181 +// 创建弹幕对象
182 +const createDanmu = (data) => {
183 + const id = `danmu_${danmuId.value++}`
184 + const containerWidth = 750 // 小程序默认宽度
185 + const danmuWidth = 350 // 弹幕宽度
186 + const extraDistance = 100 // 额外移动距离,确保完全消失
187 +
188 + return {
189 + id,
190 + data,
191 + x: containerWidth, // 从右侧开始
192 + isMoving: false,
193 + opacity: 1,
194 + duration: ((containerWidth + danmuWidth + extraDistance) / (props.danmuSpeed * 0.6)) * 1000, // 减慢速度,让弹幕在屏幕上停留更久
195 + startTime: Date.now()
196 + }
197 +}
198 +
199 +// 添加弹幕到轨道
200 +const addDanmuToTrack = (danmu) => {
201 + const track = findAvailableTrack()
202 + if (!track) return false
203 +
204 + track.danmus.push(danmu)
205 + track.lastDanmuTime = Date.now()
206 +
207 + // 启动弹幕动画
208 + nextTick(() => {
209 + startDanmuAnimation(danmu, track)
210 + })
211 +
212 + return true
213 +}
214 +
215 +// 启动弹幕动画
216 +const startDanmuAnimation = (danmu, track) => {
217 + if (!isPlaying.value) return
218 +
219 + // 清除之前的定时器(如果存在)
220 + if (animationTimers.value.has(danmu.id)) {
221 + clearTimeout(animationTimers.value.get(danmu.id))
222 + animationTimers.value.delete(danmu.id)
223 + }
224 +
225 + // 设置动画 - 从右侧移动到左侧屏幕外
226 + danmu.isMoving = true
227 +
228 + // 使用 nextTick 确保 DOM 更新后再启动动画
229 + nextTick(() => {
230 + // 移动到左侧屏幕外,确保弹幕完全消失
231 + danmu.x = -(450) // 移动距离 = 弹幕宽度(350) + 额外距离(100)
232 + })
233 +
234 + // 监听动画结束事件,实现循环
235 + const timer = setTimeout(() => {
236 + // 动画结束后,重置弹幕位置到右侧,开始新的循环
237 + resetDanmuForLoop(danmu, track)
238 + }, danmu.duration)
239 +
240 + animationTimers.value.set(danmu.id, timer)
241 +}
242 +
243 +// 重置弹幕位置实现循环
244 +const resetDanmuForLoop = (danmu, track) => {
245 + if (!isPlaying.value) return
246 +
247 + // 立即停止当前动画
248 + danmu.isMoving = false
249 +
250 + // 使用 nextTick 确保 DOM 更新
251 + nextTick(() => {
252 + // 重置弹幕位置到右侧
253 + danmu.x = 750 // 回到右侧起始位置
254 +
255 + // 短暂延迟后重新开始动画
256 + setTimeout(() => {
257 + if (isPlaying.value) {
258 + startDanmuAnimation(danmu, track)
259 + }
260 + }, 50) // 减少延迟时间
261 + })
262 +}
263 +
264 +// 从轨道移除弹幕
265 +const removeDanmuFromTrack = (danmu, track) => {
266 + const index = track.danmus.findIndex(d => d.id === danmu.id)
267 + if (index > -1) {
268 + track.danmus.splice(index, 1)
269 + }
270 +}
271 +
272 +// 发送弹幕
273 +const sendDanmu = (data) => {
274 + if (!isPlaying.value) return
275 +
276 + const danmu = createDanmu(data)
277 + addDanmuToTrack(danmu)
278 +}
279 +
280 +// 当前弹幕索引,用于顺序显示
281 +let currentDanmuIndex = 0
282 +
283 +// 发送顺序弹幕
284 +const sendSequentialDanmu = () => {
285 + // 按顺序获取弹幕数据
286 + const danmuData = mockFamilyDanmus[currentDanmuIndex % mockFamilyDanmus.length]
287 + sendDanmu(danmuData)
288 +
289 + // 更新索引,循环使用
290 + currentDanmuIndex++
291 +}
292 +
293 +// 自动发送弹幕
294 +let autoSendTimer = null
295 +const startAutoSend = () => {
296 + if (autoSendTimer) return
297 +
298 + const sendNext = () => {
299 + if (isPlaying.value) {
300 + // 移除弹幕数量限制,直接发送弹幕
301 + sendSequentialDanmu()
302 + }
303 + // 优化发送间隔,在保持合理间距的同时增加密度
304 + autoSendTimer = setTimeout(sendNext, 1000 + Math.random() * 600) // 1.0-1.6秒随机间隔,适度增加发送频率
305 + }
306 +
307 + sendNext()
308 +}
309 +
310 +const stopAutoSend = () => {
311 + if (autoSendTimer) {
312 + clearTimeout(autoSendTimer)
313 + autoSendTimer = null
314 + }
315 +}
316 +
317 +// 暂停/播放弹幕
318 +const toggleDanmu = () => {
319 + isPlaying.value = !isPlaying.value
320 +
321 + if (isPlaying.value) {
322 + // 恢复播放时,重新启动所有弹幕的动画
323 + tracks.forEach(track => {
324 + track.danmus.forEach(danmu => {
325 + if (!danmu.isMoving) {
326 + startDanmuAnimation(danmu, track)
327 + }
328 + })
329 + })
330 + startAutoSend()
331 + } else {
332 + // 暂停时,停止自动发送但保持弹幕在当前位置
333 + stopAutoSend()
334 + }
335 +}
336 +
337 +// 清空所有弹幕
338 +const clearDanmu = () => {
339 + // 清除所有动画定时器
340 + animationTimers.value.forEach(timer => clearTimeout(timer))
341 + animationTimers.value.clear()
342 +
343 + // 清空所有轨道的弹幕
344 + tracks.forEach(track => {
345 + track.danmus = []
346 + track.lastDanmuTime = 0
347 + })
348 +}
349 +
350 +// 处理弹幕点击
351 +const handleDanmuClick = (danmu) => {
352 + emit('danmu-click', danmu.data)
353 +
354 + Taro.showToast({
355 + title: `点击了${danmu.data.familyName}`,
356 + icon: 'none',
357 + duration: 2000
358 + })
359 +}
360 +
361 +// 暴露方法给父组件
362 +defineExpose({
363 + toggleDanmu,
364 + clearDanmu,
365 + sendCustomDanmu: sendDanmu
366 +})
367 +
368 +// 生命周期
369 +onMounted(() => {
370 + // 自动开始播放并发送初始弹幕
371 + isPlaying.value = true
372 +
373 + // 页面加载后,优化初始弹幕发送,保持合理间距和密度
374 + setTimeout(() => {
375 + for (let i = 0; i < 4; i++) { // 适度增加初始弹幕数量
376 + setTimeout(() => {
377 + sendSequentialDanmu()
378 + }, i * 800) // 调整发送间隔到0.8秒,平衡密度和间距
379 + }
380 + }, 400) // 减少初始延迟
381 +
382 + // 启动自动发送
383 + startAutoSend()
384 +})
385 +
386 +onUnmounted(() => {
387 + stopAutoSend()
388 + clearDanmu()
389 +})
390 +</script>
391 +
392 +<style >
393 +.native-danmu-container {
394 + width: 100%;
395 + position: relative;
396 +}
397 +
398 +.danmu-display-area {
399 + width: 100%;
400 + /* background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); */
401 + /* border-radius: 20rpx; */
402 + position: relative;
403 + overflow: hidden;
404 + /* box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1); */
405 +}
406 +
407 +.danmu-track {
408 + position: absolute;
409 + width: 100%;
410 + left: 0;
411 + pointer-events: none;
412 +}
413 +
414 +.danmu-item {
415 + position: absolute;
416 + top: 50%;
417 + transform: translateY(-50%);
418 + pointer-events: auto;
419 + z-index: 10;
420 + right: 0; /* 从右侧开始 */
421 +}
422 +
423 +.danmu-content {
424 + display: flex;
425 + align-items: center;
426 + background: rgba(255, 255, 255, 0.65);
427 + border-radius: 30rpx;
428 + padding: 16rpx 20rpx;
429 + box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
430 + backdrop-filter: blur(12rpx);
431 + border: 2rpx solid rgba(255, 255, 255, 0.4);
432 + max-width: 350rpx;
433 + min-width: 300rpx;
434 + margin: 8rpx 0;
435 + transition: all 0.3s ease;
436 +}
437 +
438 +.danmu-avatar-container {
439 + flex-shrink: 0;
440 + margin-right: 16rpx;
441 + position: relative;
442 +}
443 +
444 +.danmu-avatar {
445 + width: 56rpx;
446 + height: 56rpx;
447 + border-radius: 50%;
448 + border: 3rpx solid #fff;
449 + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
450 +}
451 +
452 +.danmu-avatar-container::after {
453 + content: '';
454 + position: absolute;
455 + top: -2rpx;
456 + left: -2rpx;
457 + right: -2rpx;
458 + bottom: -2rpx;
459 + border-radius: 50%;
460 + background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4);
461 + z-index: -1;
462 + animation: rotate 3s linear infinite;
463 +}
464 +
465 +@keyframes rotate {
466 + from { transform: rotate(0deg); }
467 + to { transform: rotate(360deg); }
468 +}
469 +
470 +.danmu-info {
471 + flex: 1;
472 + min-width: 0;
473 +}
474 +
475 +.danmu-title-row {
476 + display: flex;
477 + align-items: center;
478 + margin-bottom: 6rpx;
479 +}
480 +
481 +.family-name {
482 + font-weight: 700;
483 + color: #1a202c;
484 + font-size: 28rpx;
485 + margin-right: 12rpx;
486 + flex-shrink: 0;
487 + text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.1);
488 +}
489 +
490 +.member-badge {
491 + background: linear-gradient(45deg, #667eea, #764ba2);
492 + color: white;
493 + padding: 4rpx 12rpx;
494 + border-radius: 16rpx;
495 + font-size: 20rpx;
496 + font-weight: 600;
497 + flex-shrink: 0;
498 + box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.3);
499 +}
500 +
501 +.family-intro {
502 + color: #4a5568;
503 + font-size: 24rpx;
504 + display: -webkit-box;
505 + margin-bottom: 6rpx;
506 + overflow: hidden;
507 + text-overflow: ellipsis;
508 + -webkit-line-clamp: 3;
509 + -webkit-box-orient: vertical;
510 + line-height: 1.4;
511 + font-weight: 500;
512 +}
513 +
514 +.danmu-bottom-row {
515 + display: flex;
516 + align-items: center;
517 + justify-content: space-between;
518 +}
519 +
520 +.family-motto {
521 + color: #2d3748;
522 + font-size: 22rpx;
523 + font-style: italic;
524 + flex: 1;
525 + overflow: hidden;
526 + text-overflow: ellipsis;
527 + white-space: nowrap;
528 + margin-right: 12rpx;
529 + opacity: 0.8;
530 +}
531 +
532 +.steps-badge {
533 + background: linear-gradient(45deg, #48bb78, #38a169);
534 + color: white;
535 + padding: 4rpx 12rpx;
536 + border-radius: 14rpx;
537 + font-size: 20rpx;
538 + font-weight: 600;
539 + flex-shrink: 0;
540 + box-shadow: 0 2rpx 8rpx rgba(72, 187, 120, 0.3);
541 +}
542 +
543 +.danmu-controls {
544 + display: flex;
545 + justify-content: center;
546 + gap: 20rpx;
547 + margin-top: 20rpx;
548 + padding: 0 20rpx;
549 +}
550 +
551 +.control-btn {
552 + flex: 1;
553 + max-width: 200rpx;
554 + height: 60rpx;
555 + border-radius: 30rpx;
556 + font-size: 24rpx;
557 +}
558 +
559 +/* 弹幕动画效果 */
560 +.danmu-moving {
561 + transition-timing-function: linear;
562 + transition-property: transform;
563 +}
564 +
565 +/* 弹幕项基础样式 */
566 +.danmu-item {
567 + position: absolute;
568 + z-index: 10;
569 + will-change: transform;
570 + backface-visibility: hidden;
571 + transform-style: preserve-3d;
572 + left: 0;
573 + top: 0;
574 + width: auto;
575 + height: auto;
576 +}
577 +
578 +/* 悬停效果 */
579 +.danmu-item:active .danmu-content {
580 + transform: scale(0.98);
581 + box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.18);
582 + transition: all 0.2s ease;
583 +}
584 +
585 +.danmu-content:hover {
586 + transform: translateY(-2rpx);
587 + box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.16);
588 +}
589 +
590 +/* 响应式设计 */
591 +@media (max-width: 750rpx) {
592 + .danmu-content {
593 + max-width: 300rpx;
594 + min-width: 240rpx;
595 + padding: 10rpx 16rpx;
596 + }
597 +
598 + .danmu-avatar {
599 + width: 40rpx;
600 + height: 40rpx;
601 + margin-right: 12rpx;
602 + }
603 +
604 + .family-name {
605 + font-size: 24rpx;
606 + }
607 +
608 + .family-intro {
609 + font-size: 20rpx;
610 + }
611 +}
612 +</style>
1 <!-- 1 <!--
2 * @Date: 2025-01-09 00:00:00 2 * @Date: 2025-01-09 00:00:00
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-10-28 13:33:56 4 + * @LastEditTime: 2025-10-28 16:24:46
5 * @FilePath: /lls_program/src/components/RankingCard.vue 5 * @FilePath: /lls_program/src/components/RankingCard.vue
6 * @Description: 排行榜卡片组件 6 * @Description: 排行榜卡片组件
7 --> 7 -->
8 <template> 8 <template>
9 <view class="ranking-card bg-blue-500 bg-opacity-50"> 9 <view class="ranking-card bg-blue-500 bg-opacity-50">
10 <!-- 卡片头部 --> 10 <!-- 卡片头部 -->
11 - <view class="card-header"> 11 + <view class="card-header" style="margin: 40rpx;">
12 <view class="card-title flex items-center"> 12 <view class="card-title flex items-center">
13 <IconFont size="20" name="https://cdn.ipadbiz.cn/lls_prog/icon/%E5%A5%96%E6%9D%AF.png" /> 13 <IconFont size="20" name="https://cdn.ipadbiz.cn/lls_prog/icon/%E5%A5%96%E6%9D%AF.png" />
14 <text class="ml-2">家庭步数总榜</text> 14 <text class="ml-2">家庭步数总榜</text>
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
17 </view> 17 </view>
18 18
19 <!-- 顶部导航 --> 19 <!-- 顶部导航 -->
20 - <view class="nav-tabs"> 20 + <view class="nav-tabs" style="margin: 40rpx;">
21 <!-- 滑动指示器 --> 21 <!-- 滑动指示器 -->
22 <view 22 <view
23 class="tab-indicator" 23 class="tab-indicator"
...@@ -39,12 +39,12 @@ ...@@ -39,12 +39,12 @@
39 </view> 39 </view>
40 40
41 <!-- 排行榜内容 --> 41 <!-- 排行榜内容 -->
42 - <view class="rank-content" :class="{ 'content-switching': isContentSwitching }"> 42 + <view class="rank-content" :class="{ 'content-switching': isContentSwitching }" style="margin: 40rpx;">
43 <!-- 排行榜日期 --> 43 <!-- 排行榜日期 -->
44 <view class="rank-date relative"> 44 <view class="rank-date relative">
45 <!-- <view class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text v-if="activeTab !== 'shanghai'" class="ml-2">排名</text><IconFont name="ask" size="16" class="ml-2" @click="handleRankAskClick"></IconFont></view> --> 45 <!-- <view class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text v-if="activeTab !== 'shanghai'" class="ml-2">排名</text><IconFont name="ask" size="16" class="ml-2" @click="handleRankAskClick"></IconFont></view> -->
46 <view class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text class="ml-2">排名</text><IconFont name="ask" size="16" class="ml-2" @click="handleRankAskClick"></IconFont></view> 46 <view class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text class="ml-2">排名</text><IconFont name="ask" size="16" class="ml-2" @click="handleRankAskClick"></IconFont></view>
47 - <view v-if="activeTab === 'support'" class="absolute font-bold text-white bg-orange-500 top-0 rounded-full px-4 py-1" style="right: 0rpx; top: 50rpx; font-size: 23rpx; z-index: 999;" @tap="joinOrganization">助力码</view> 47 + <view v-if="activeTab === 'support'" class="absolute font-bold text-white bg-orange-500 top-0 rounded-full px-4 py-1" style="right: -30rpx; top: -5rpx; font-size: 23rpx; z-index: 999;" @tap="joinOrganization">助力码</view>
48 </view> 48 </view>
49 49
50 <!-- <view v-if="activeTab === 'shanghai'" class="relative mb-2 text-white"> 50 <!-- <view v-if="activeTab === 'shanghai'" class="relative mb-2 text-white">
...@@ -124,9 +124,21 @@ ...@@ -124,9 +124,21 @@
124 </view> 124 </view>
125 </view> 125 </view>
126 126
127 + <!-- 弹幕显示助力榜内容 -->
128 + <view v-if="activeTab === 'support'" class="danmu-section">
129 + <NativeDanmuComponent
130 + :container-height="700"
131 + :show-controls="true"
132 + :track-count="3"
133 + @danmu-click="handleDanmuClick"
134 + @danmu-hover="handleDanmuHover"
135 + ref="danmuRef"
136 + />
137 + </view>
138 +
127 <!-- 我的排名卡片 --> 139 <!-- 我的排名卡片 -->
128 <!-- 特殊写法 因为直接写一个排名卡片排名数字有被截断问题 --> 140 <!-- 特殊写法 因为直接写一个排名卡片排名数字有被截断问题 -->
129 - <view v-if="myRank && topRanks.length > 0 && activeTab !== 'support'" class="my-rank-section"> 141 + <view v-if="myRank && topRanks.length > 0 && activeTab !== 'support'" class="my-rank-section" style="margin: 40rpx;">
130 <view class="my-rank-content"> 142 <view class="my-rank-content">
131 <view class="my-rank-left"> 143 <view class="my-rank-left">
132 <view class="my-rank-number"> 144 <view class="my-rank-number">
...@@ -145,7 +157,7 @@ ...@@ -145,7 +157,7 @@
145 </view> 157 </view>
146 </view> 158 </view>
147 </view> 159 </view>
148 - <view v-if="myRank && !topRanks.length && activeTab !== 'support'" class="my-rank-section"> 160 + <view v-if="myRank && !topRanks.length && activeTab !== 'support'" class="my-rank-section" style="margin: 40rpx;">
149 <view class="my-rank-content"> 161 <view class="my-rank-content">
150 <view class="my-rank-left"> 162 <view class="my-rank-left">
151 <view class="my-rank-number"> 163 <view class="my-rank-number">
...@@ -192,7 +204,8 @@ ...@@ -192,7 +204,8 @@
192 import { ref, computed, onMounted, watch, nextTick } from 'vue' 204 import { ref, computed, onMounted, watch, nextTick } from 'vue'
193 import Taro from '@tarojs/taro' 205 import Taro from '@tarojs/taro'
194 import { IconFont } from '@nutui/icons-vue-taro'; 206 import { IconFont } from '@nutui/icons-vue-taro';
195 -import NumberRoll from './NumberRoll.vue' 207 +// import NumberRoll from './NumberRoll.vue'
208 +import NativeDanmuComponent from '@/components/NativeDanmuComponent.vue'
196 // 默认头像 209 // 默认头像
197 const defaultAvatar = 'https://cdn.ipadbiz.cn/lls_prog/images/%E5%85%A8%E5%AE%B6%E7%A6%8F3_%E5%89%AF%E6%9C%AC.jpg?imageMogr2/strip/quality/60' 210 const defaultAvatar = 'https://cdn.ipadbiz.cn/lls_prog/images/%E5%85%A8%E5%AE%B6%E7%A6%8F3_%E5%89%AF%E6%9C%AC.jpg?imageMogr2/strip/quality/60'
198 // 助力榜图片 211 // 助力榜图片
...@@ -510,6 +523,27 @@ const refreshData = async () => { ...@@ -510,6 +523,27 @@ const refreshData = async () => {
510 await loadLeaderboardData(true) 523 await loadLeaderboardData(true)
511 } 524 }
512 525
526 +
527 +/**
528 + * 弹幕组件相关
529 + */
530 +const danmuRef = ref(null)
531 +
532 +// 处理弹幕点击事件
533 +const handleDanmuClick = (familyData) => {
534 + console.log('弹幕点击:', familyData)
535 + Taro.showToast({
536 + title: `点击了${familyData.familyName}`,
537 + icon: 'none',
538 + duration: 2000
539 + })
540 +}
541 +
542 +// 处理弹幕悬停事件
543 +const handleDanmuHover = (familyData) => {
544 + console.log('弹幕悬停:', familyData)
545 +}
546 +
513 /** 547 /**
514 * 页面初始化 548 * 页面初始化
515 */ 549 */
...@@ -539,7 +573,7 @@ defineExpose({ ...@@ -539,7 +573,7 @@ defineExpose({
539 // background: linear-gradient(180deg, var(--primary-color) 0%, var(--primary-color) 100%); 573 // background: linear-gradient(180deg, var(--primary-color) 0%, var(--primary-color) 100%);
540 // background: var(--primary-color); 574 // background: var(--primary-color);
541 border-radius: 20rpx; 575 border-radius: 20rpx;
542 - padding: 40rpx; 576 + // padding: 40rpx;
543 margin: 30rpx; 577 margin: 30rpx;
544 margin-bottom: 0; 578 margin-bottom: 0;
545 position: relative; 579 position: relative;
......
1 <!-- 1 <!--
2 * @Date: 2025-09-01 13:07:52 2 * @Date: 2025-09-01 13:07:52
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-10-28 13:32:48 4 + * @LastEditTime: 2025-10-28 16:04:30
5 * @FilePath: /lls_program/src/pages/FamilyRank/index.vue 5 * @FilePath: /lls_program/src/pages/FamilyRank/index.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
42 <view v-if="!loading" class="rank-date relative"> 42 <view v-if="!loading" class="rank-date relative">
43 <!-- <view class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text v-if="activeTab !== 'shanghai'" class="ml-2">排名</text><IconFont name="ask" size="14" class="ml-2" @click="handleRankAskClick"></IconFont></view> --> 43 <!-- <view class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text v-if="activeTab !== 'shanghai'" class="ml-2">排名</text><IconFont name="ask" size="14" class="ml-2" @click="handleRankAskClick"></IconFont></view> -->
44 <view class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text class="ml-2">排名</text><IconFont name="ask" size="14" class="ml-2" @click="handleRankAskClick"></IconFont></view> 44 <view class="flex items-center justify-center"><text class="mr-2">截止昨日</text>{{ currentDate }}<text class="ml-2">排名</text><IconFont name="ask" size="14" class="ml-2" @click="handleRankAskClick"></IconFont></view>
45 - <view v-if="activeTab === 'support'" class="absolute font-bold text-white bg-orange-500 top-0 rounded-full px-4 py-1" style="right: 40rpx; top: 50rpx; font-size: 23rpx;" @tap="joinOrganization">助力码</view> 45 + <view v-if="activeTab === 'support'" class="absolute font-bold text-white bg-orange-500 top-0 rounded-full px-4 py-1" style="right: 45rpx; top: -5rpx; font-size: 23rpx;" @tap="joinOrganization">助力码</view>
46 </view> 46 </view>
47 47
48 <!-- 参与家庭数量显示 --> 48 <!-- 参与家庭数量显示 -->
...@@ -154,7 +154,18 @@ ...@@ -154,7 +154,18 @@
154 <view class="no-data-text">暂无助力排行榜更多数据</view> 154 <view class="no-data-text">暂无助力排行榜更多数据</view>
155 </view> --> 155 </view> -->
156 156
157 - <!-- 弹幕显示助力榜内容 --> 157 + </view>
158 +
159 + <!-- 弹幕显示助力榜内容 -->
160 + <view v-if="activeTab === 'support'" class="danmu-section mt-8">
161 + <NativeDanmuComponent
162 + :container-height="1200"
163 + :show-controls="true"
164 + :track-count="7"
165 + @danmu-click="handleDanmuClick"
166 + @danmu-hover="handleDanmuHover"
167 + ref="danmuRef"
168 + />
158 </view> 169 </view>
159 170
160 <!-- 我的排名悬浮卡片 --> 171 <!-- 我的排名悬浮卡片 -->
...@@ -209,8 +220,8 @@ import { ref, computed, onMounted } from 'vue' ...@@ -209,8 +220,8 @@ import { ref, computed, onMounted } from 'vue'
209 import Taro from '@tarojs/taro' 220 import Taro from '@tarojs/taro'
210 import { IconFont } from '@nutui/icons-vue-taro'; 221 import { IconFont } from '@nutui/icons-vue-taro';
211 import BackToTop from '@/components/BackToTop.vue' 222 import BackToTop from '@/components/BackToTop.vue'
223 +import NativeDanmuComponent from '@/components/NativeDanmuComponent.vue'
212 // import NumberRoll from '@/components/NumberRoll.vue' 224 // import NumberRoll from '@/components/NumberRoll.vue'
213 -import vueDanmaku from 'vue-danmaku'
214 // 默认头像 225 // 默认头像
215 const defaultAvatar = 'https://cdn.ipadbiz.cn/lls_prog/images/%E5%85%A8%E5%AE%B6%E7%A6%8F3_%E5%89%AF%E6%9C%AC.jpg?imageMogr2/strip/quality/60' 226 const defaultAvatar = 'https://cdn.ipadbiz.cn/lls_prog/images/%E5%85%A8%E5%AE%B6%E7%A6%8F3_%E5%89%AF%E6%9C%AC.jpg?imageMogr2/strip/quality/60'
216 const supportImg = 'https://cdn.ipadbiz.cn/lls_prog/images/support_img_1.png' 227 const supportImg = 'https://cdn.ipadbiz.cn/lls_prog/images/support_img_1.png'
...@@ -268,6 +279,26 @@ const handleRankAskClick = () => { ...@@ -268,6 +279,26 @@ const handleRankAskClick = () => {
268 } 279 }
269 280
270 /** 281 /**
282 + * 弹幕组件相关
283 + */
284 +const danmuRef = ref(null)
285 +
286 +// 处理弹幕点击事件
287 +const handleDanmuClick = (familyData) => {
288 + console.log('弹幕点击:', familyData)
289 + Taro.showToast({
290 + title: `点击了${familyData.familyName}`,
291 + icon: 'none',
292 + duration: 2000
293 + })
294 +}
295 +
296 +// 处理弹幕悬停事件
297 +const handleDanmuHover = (familyData) => {
298 + console.log('弹幕悬停:', familyData)
299 +}
300 +
301 +/**
271 * 触发数字滚动动画 302 * 触发数字滚动动画
272 */ 303 */
273 const triggerNumberRoll = () => { 304 const triggerNumberRoll = () => {
...@@ -1039,7 +1070,5 @@ onMounted(async () => { ...@@ -1039,7 +1070,5 @@ onMounted(async () => {
1039 } 1070 }
1040 } 1071 }
1041 } 1072 }
1042 -
1043 -
1044 } 1073 }
1045 </style> 1074 </style>
......