feat(弹幕组件): 添加原生弹幕组件并集成到排行榜页面
- 新增 NativeDanmuComponent 原生弹幕组件,支持多轨道弹幕展示 - 在家庭排行榜页面集成弹幕功能,展示助力榜内容 - 调整排行榜卡片布局和样式,优化助力码按钮位置 - 添加弹幕点击和悬停事件处理逻辑
Showing
4 changed files
with
690 additions
and
14 deletions
| ... | @@ -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'] | ... | ... |
src/components/NativeDanmuComponent.vue
0 → 100644
| 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 | + </view> | ||
| 158 | + | ||
| 157 | <!-- 弹幕显示助力榜内容 --> | 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> | ... | ... |
-
Please register or login to post a comment