hookehuyr

feat(排行榜): 添加排行榜卡片组件并更新主题颜色

- 新增 RankingCard 组件用于展示活动步数排行榜
- 将主题蓝色从 #4A90E2 更新为 #54ABAE
- 替换 Dashboard 页面的排行榜视图为新的 RankingCard 组件
...@@ -28,6 +28,7 @@ declare module 'vue' { ...@@ -28,6 +28,7 @@ declare module 'vue' {
28 PointsCollector: typeof import('./src/components/PointsCollector.vue')['default'] 28 PointsCollector: typeof import('./src/components/PointsCollector.vue')['default']
29 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] 29 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
30 PrimaryButton: typeof import('./src/components/PrimaryButton.vue')['default'] 30 PrimaryButton: typeof import('./src/components/PrimaryButton.vue')['default']
31 + RankingCard: typeof import('./src/components/RankingCard.vue')['default']
31 RouterLink: typeof import('vue-router')['RouterLink'] 32 RouterLink: typeof import('vue-router')['RouterLink']
32 RouterView: typeof import('vue-router')['RouterView'] 33 RouterView: typeof import('vue-router')['RouterView']
33 ShareButton: typeof import('./src/components/ShareButton/index.vue')['default'] 34 ShareButton: typeof import('./src/components/ShareButton/index.vue')['default']
......
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
3 @tailwind utilities; 3 @tailwind utilities;
4 4
5 .bg-blue-500 { 5 .bg-blue-500 {
6 - background-color: #4A90E2 !important; 6 + background-color: #54ABAE !important;
7 } 7 }
8 8
9 .text-blue-500 { 9 .text-blue-500 {
10 - color: #4A90E2 !important; 10 + color: #54ABAE !important;
11 } 11 }
12 12
13 .border-blue-500 { 13 .border-blue-500 {
14 - border-color: #4A90E2 !important; 14 + border-color: #54ABAE !important;
15 } 15 }
......
1 +<!--
2 + * @Date: 2025-01-09 00:00:00
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-09-08 20:22:19
5 + * @FilePath: /lls_program/src/components/RankingCard.vue
6 + * @Description: 排行榜卡片组件
7 +-->
8 +<template>
9 + <view class="ranking-card">
10 + <!-- 卡片头部 -->
11 + <view class="card-header">
12 + <view class="card-title">全市排行</view>
13 + <view class="view-more" @tap="handleViewMore">查看更多</view>
14 + </view>
15 +
16 + <!-- 顶部导航 -->
17 + <view class="nav-tabs">
18 + <!-- 滑动指示器 -->
19 + <view
20 + class="tab-indicator"
21 + :class="{ 'indicator-shanghai': activeTab === 'shanghai' }"
22 + ></view>
23 + <view
24 + class="tab-item"
25 + :class="{ active: activeTab === 'huangpu' }"
26 + @click="switchTab('huangpu')"
27 + >
28 + 黄埔榜
29 + </view>
30 + <view
31 + class="tab-item"
32 + :class="{ active: activeTab === 'shanghai' }"
33 + @click="switchTab('shanghai')"
34 + >
35 + 上海榜
36 + </view>
37 + </view>
38 +
39 + <!-- 排行榜内容 -->
40 + <view class="rank-content" :class="{ 'content-switching': isContentSwitching }">
41 + <!-- 前三名展示 -->
42 + <view class="top-three">
43 + <!-- 第二名 -->
44 + <view class="rank-item second">
45 + <view class="crown crown-silver">👑</view>
46 + <view class="avatar">
47 + <image :src="topRanks[1]?.avatar" class="avatar-img" mode="aspectFill" />
48 + </view>
49 + <view class="family-name">{{ topRanks[1]?.familyName }}</view>
50 + <view class="leader-name">大家长:{{ topRanks[1]?.leaderName }}</view>
51 + <view class="rank-number">
52 + <view class="rank-num">2</view>
53 + <view class="steps-in-rank">{{ formatSteps(topRanks[1]?.steps) }}</view>
54 + </view>
55 + </view>
56 +
57 + <!-- 第一名 -->
58 + <view class="rank-item first">
59 + <view class="crown crown-gold">👑</view>
60 + <view class="avatar">
61 + <image :src="topRanks[0]?.avatar" class="avatar-img" mode="aspectFill" />
62 + </view>
63 + <view class="family-name">{{ topRanks[0]?.familyName }}</view>
64 + <view class="leader-name">大家长:{{ topRanks[0]?.leaderName }}</view>
65 + <view class="rank-number">
66 + <view class="rank-num">1</view>
67 + <view class="steps-in-rank">{{ formatSteps(topRanks[0]?.steps) }}</view>
68 + </view>
69 + </view>
70 +
71 + <!-- 第三名 -->
72 + <view class="rank-item third">
73 + <view class="crown crown-bronze">👑</view>
74 + <view class="avatar">
75 + <image :src="topRanks[2]?.avatar" class="avatar-img" mode="aspectFill" />
76 + </view>
77 + <view class="family-name">{{ topRanks[2]?.familyName }}</view>
78 + <view class="leader-name">大家长:{{ topRanks[2]?.leaderName }}</view>
79 + <view class="rank-number">
80 + <view class="rank-num">3</view>
81 + <view class="steps-in-rank">{{ formatSteps(topRanks[2]?.steps) }}</view>
82 + </view>
83 + </view>
84 + </view>
85 + </view>
86 +
87 + <!-- 我的排名卡片 -->
88 + <view class="my-rank-section">
89 + <view class="my-rank-content">
90 + <view class="my-rank-left">
91 + <view class="my-rank-number">{{ myRank.rank }}+</view>
92 + <view class="my-avatar">
93 + <image :src="myRank.avatar" class="my-avatar-img" mode="aspectFill" />
94 + </view>
95 + <view class="my-family-info">
96 + <view class="my-family-name">{{ myRank.familyName }}</view>
97 + <view class="my-leader-name">大家长:{{ myRank.leaderName }}</view>
98 + </view>
99 + </view>
100 + <view class="my-rank-right">
101 + <view class="my-steps">{{ formatStepsForList(myRank.steps) }}</view>
102 + <view class="rank-status">{{ myRank.status }}</view>
103 + </view>
104 + </view>
105 + </view>
106 + </view>
107 +</template>
108 +
109 +<script setup>
110 +import { ref } from 'vue'
111 +
112 +// 定义props
113 +const props = defineProps({
114 + onViewMore: {
115 + type: Function,
116 + default: () => {}
117 + }
118 +})
119 +
120 +// 当前激活的tab
121 +const activeTab = ref('huangpu')
122 +
123 +// 内容切换状态
124 +const isContentSwitching = ref(false)
125 +
126 +/**
127 + * 切换tab
128 + * @param {string} tab - tab名称
129 + */
130 +const switchTab = (tab) => {
131 + if (activeTab.value === tab) return
132 +
133 + // 开始切换动画
134 + isContentSwitching.value = true
135 +
136 + // 延迟切换内容,让淡出动画先执行
137 + setTimeout(() => {
138 + activeTab.value = tab
139 +
140 + // 内容切换后,结束切换状态,开始淡入动画
141 + setTimeout(() => {
142 + isContentSwitching.value = false
143 + }, 50)
144 + }, 200)
145 +}
146 +
147 +/**
148 + * 格式化步数显示
149 + * @param {number} steps - 步数
150 + * @returns {string} 格式化后的步数
151 + */
152 +const formatSteps = (steps) => {
153 + return steps ? steps.toLocaleString() : '0'
154 +}
155 +
156 +/**
157 + * 格式化步数显示(用于列表显示,使用千位分隔符格式)
158 + * @param {number} steps - 步数
159 + * @returns {string} 格式化后的步数
160 + */
161 +const formatStepsForList = (steps) => {
162 + return steps ? steps.toLocaleString() : '0'
163 +}
164 +
165 +/**
166 + * 处理查看更多点击事件
167 + */
168 +const handleViewMore = () => {
169 + if (props.onViewMore) {
170 + props.onViewMore()
171 + }
172 +}
173 +
174 +// 前三名数据
175 +const topRanks = ref([
176 + {
177 + rank: 1,
178 + familyName: '明媚的晴',
179 + leaderName: '张明',
180 + avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
181 + steps: 45670
182 + },
183 + {
184 + rank: 2,
185 + familyName: '甜心小桃',
186 + leaderName: '李桃',
187 + avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
188 + steps: 42350
189 + },
190 + {
191 + rank: 3,
192 + familyName: '真心找爱',
193 + leaderName: '王真',
194 + avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
195 + steps: 38920
196 + }
197 +])
198 +
199 +// 我的排名信息
200 +const myRank = ref({
201 + rank: 99,
202 + familyName: '和谐之家',
203 + leaderName: '陈家明',
204 + avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
205 + steps: 8920,
206 + status: '未上榜'
207 +})
208 +</script>
209 +
210 +<style lang="less">
211 +.ranking-card {
212 + background: linear-gradient(180deg, #4A90E2 0%, #357ABD 100%);
213 + border-radius: 20rpx;
214 + padding: 40rpx;
215 + margin: 30rpx;
216 + margin-bottom: 0;
217 + position: relative;
218 + overflow: hidden;
219 +
220 + .card-header {
221 + display: flex;
222 + justify-content: space-between;
223 + align-items: center;
224 + margin-bottom: 30rpx;
225 +
226 + .card-title {
227 + color: white;
228 + font-size: 32rpx;
229 + font-weight: 600;
230 + }
231 +
232 + .view-more {
233 + color: rgba(255, 255, 255, 0.8);
234 + font-size: 24rpx;
235 + padding: 8rpx 16rpx;
236 + border: 1rpx solid rgba(255, 255, 255, 0.3);
237 + border-radius: 20rpx;
238 + transition: all 0.3s ease;
239 +
240 + &:hover {
241 + background: rgba(255, 255, 255, 0.1);
242 + color: white;
243 + }
244 + }
245 + }
246 +
247 + .nav-tabs {
248 + display: flex;
249 + justify-content: center;
250 + background: rgba(255, 255, 255, 0.2);
251 + border-radius: 60rpx;
252 + padding: 8rpx;
253 + margin-bottom: 40rpx;
254 + position: relative;
255 + overflow: hidden;
256 +
257 + .tab-indicator {
258 + position: absolute;
259 + top: 8rpx;
260 + left: 8rpx;
261 + width: calc(50% - 8rpx);
262 + height: calc(100% - 16rpx);
263 + background: rgba(255, 255, 255, 1);
264 + border-radius: 52rpx;
265 + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
266 + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
267 + z-index: 1;
268 +
269 + &.indicator-shanghai {
270 + transform: translateX(100%);
271 + }
272 + }
273 +
274 + .tab-item {
275 + flex: 1;
276 + padding: 20rpx 0;
277 + text-align: center;
278 + border-radius: 52rpx;
279 + color: rgba(255, 255, 255, 0.8);
280 + font-size: 28rpx;
281 + font-weight: 500;
282 + transition: all 0.3s ease;
283 + position: relative;
284 + z-index: 2;
285 + cursor: pointer;
286 +
287 + &.active {
288 + color: #4A90E2;
289 + font-weight: 600;
290 + transform: scale(1.02);
291 + }
292 +
293 + &:hover {
294 + color: rgba(255, 255, 255, 1);
295 + }
296 + }
297 + }
298 +
299 + .rank-content {
300 + transition: all 0.3s ease;
301 + transform: translateY(0);
302 + opacity: 1;
303 +
304 + &.content-switching {
305 + opacity: 0.3;
306 + transform: translateY(-20rpx);
307 + }
308 + }
309 +
310 + .top-three {
311 + position: relative;
312 + display: flex;
313 + justify-content: center;
314 + align-items: flex-end;
315 + margin-bottom: 40rpx;
316 + // height: 280rpx;
317 + gap: -20rpx;
318 +
319 + .rank-item {
320 + display: flex;
321 + flex-direction: column;
322 + align-items: center;
323 + position: relative;
324 + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
325 +
326 + .crown {
327 + font-size: 30rpx;
328 + margin-bottom: 8rpx;
329 +
330 + &.crown-gold {
331 + filter: grayscale(0.3) brightness(1.1);
332 + }
333 +
334 + &.crown-silver {
335 + filter: hue-rotate(45deg) brightness(1.2);
336 + }
337 +
338 + &.crown-bronze {
339 + filter: hue-rotate(25deg) brightness(0.9);
340 + }
341 + }
342 +
343 + .avatar {
344 + width: 80rpx;
345 + height: 80rpx;
346 + border-radius: 50%;
347 + overflow: hidden;
348 + border: 4rpx solid rgba(255, 255, 255, 0.8);
349 + margin-bottom: 12rpx;
350 +
351 + .avatar-img {
352 + width: 100%;
353 + height: 100%;
354 + object-fit: cover;
355 + }
356 + }
357 +
358 + .family-name {
359 + color: white;
360 + font-size: 20rpx;
361 + font-weight: 600;
362 + margin-bottom: 6rpx;
363 + text-align: center;
364 + }
365 +
366 + .leader-name {
367 + color: rgba(255, 255, 255, 0.8);
368 + font-size: 16rpx;
369 + margin-bottom: 12rpx;
370 + text-align: center;
371 + }
372 +
373 + .rank-number {
374 + width: 100rpx;
375 + height: 120rpx;
376 + display: flex;
377 + flex-direction: column;
378 + align-items: center;
379 + justify-content: center;
380 + position: relative;
381 + background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
382 + border-radius: 16rpx 16rpx 0 0;
383 + box-shadow: 0 6rpx 12rpx rgba(0, 0, 0, 0.2);
384 +
385 + .rank-num {
386 + font-size: 32rpx;
387 + font-weight: bold;
388 + color: white;
389 + margin-bottom: 8rpx;
390 + }
391 +
392 + .steps-in-rank {
393 + font-size: 22rpx;
394 + font-weight: 600;
395 + color: white;
396 + text-align: center;
397 + line-height: 1.2;
398 + }
399 + }
400 +
401 + &.second {
402 + order: 1;
403 + margin-top: 40rpx;
404 + margin-right: -10rpx;
405 + z-index: 2;
406 +
407 + .avatar {
408 + box-shadow: 0 0 10rpx rgba(192, 192, 192, 0.4);
409 + }
410 +
411 + .rank-number {
412 + width: 120rpx;
413 + height: 110rpx;
414 + background: linear-gradient(135deg, #C0C0C0 0%, #A0A0A0 100%);
415 + }
416 + }
417 +
418 + &.first {
419 + order: 2;
420 + z-index: 3;
421 +
422 + .avatar {
423 + width: 100rpx;
424 + height: 100rpx;
425 + border-color: #FFD700;
426 + box-shadow: 0 0 15rpx rgba(255, 215, 0, 0.5);
427 + }
428 +
429 + .rank-number {
430 + width: 120rpx;
431 + height: 140rpx;
432 + background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
433 + }
434 + }
435 +
436 + &.third {
437 + order: 3;
438 + margin-top: 40rpx;
439 + margin-left: -10rpx;
440 + z-index: 1;
441 +
442 + .avatar {
443 + box-shadow: 0 0 10rpx rgba(205, 127, 50, 0.4);
444 + }
445 +
446 + .rank-number {
447 + width: 120rpx;
448 + height: 100rpx;
449 + background: linear-gradient(135deg, #CD7F32 0%, #B8860B 100%);
450 + }
451 + }
452 + }
453 + }
454 +
455 + .my-rank-section {
456 + background: rgba(255, 255, 255, 0.95);
457 + border-radius: 16rpx;
458 + padding: 20rpx 24rpx;
459 + backdrop-filter: blur(10px);
460 + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
461 +
462 + .my-rank-content {
463 + display: flex;
464 + align-items: center;
465 + justify-content: space-between;
466 +
467 + .my-rank-left {
468 + display: flex;
469 + align-items: center;
470 + flex: 1;
471 +
472 + .my-rank-number {
473 + width: 48rpx;
474 + height: 48rpx;
475 + background: #4A90E2;
476 + border-radius: 50%;
477 + display: flex;
478 + align-items: center;
479 + justify-content: center;
480 + font-size: 20rpx;
481 + font-weight: bold;
482 + color: white;
483 + margin-right: 20rpx;
484 + }
485 +
486 + .my-avatar {
487 + width: 60rpx;
488 + height: 60rpx;
489 + border-radius: 50%;
490 + overflow: hidden;
491 + margin-right: 20rpx;
492 +
493 + .my-avatar-img {
494 + width: 100%;
495 + height: 100%;
496 + object-fit: cover;
497 + }
498 + }
499 +
500 + .my-family-info {
501 + flex: 1;
502 +
503 + .my-family-name {
504 + font-size: 24rpx;
505 + font-weight: 600;
506 + color: #333;
507 + margin-bottom: 6rpx;
508 + }
509 +
510 + .my-leader-name {
511 + font-size: 20rpx;
512 + color: #666;
513 + }
514 + }
515 + }
516 +
517 + .my-rank-right {
518 + text-align: right;
519 +
520 + .my-steps {
521 + font-size: 26rpx;
522 + font-weight: bold;
523 + color: #4A90E2;
524 + margin-bottom: 6rpx;
525 + }
526 +
527 + .rank-status {
528 + font-size: 20rpx;
529 + color: #999;
530 + }
531 + }
532 + }
533 + }
534 +}
535 +</style>
...@@ -132,15 +132,8 @@ ...@@ -132,15 +132,8 @@
132 </template> 132 </template>
133 </WeRunAuth> 133 </WeRunAuth>
134 134
135 - <!-- 活动排行榜 --> 135 + <!-- 排行榜卡片 -->
136 - <view class="px-5 mb-4 mt-4"> 136 + <RankingCard :onViewMore="openFamilyRank" />
137 - <view @tap="openFamilyRank" class="w-full bg-blue-500 text-white py-3 rounded-lg flex flex-col items-center justify-center">
138 - <view class="flex items-center justify-center">
139 - <Category size="16" class="mr-2" />
140 - 昨日活动步数排行榜
141 - </view>
142 - </view>
143 - </view>
144 137
145 <!-- Family album --> 138 <!-- Family album -->
146 <view class="p-5 mt-4 mb-6 bg-white rounded-xl shadow-md mx-4"> 139 <view class="p-5 mt-4 mb-6 bg-white rounded-xl shadow-md mx-4">
...@@ -232,6 +225,7 @@ import BottomNav from '../../components/BottomNav.vue'; ...@@ -232,6 +225,7 @@ import BottomNav from '../../components/BottomNav.vue';
232 import TotalPointsDisplay from '@/components/TotalPointsDisplay.vue'; 225 import TotalPointsDisplay from '@/components/TotalPointsDisplay.vue';
233 import PointsCollector from '@/components/PointsCollector.vue' 226 import PointsCollector from '@/components/PointsCollector.vue'
234 import WeRunAuth from '@/components/WeRunAuth.vue' 227 import WeRunAuth from '@/components/WeRunAuth.vue'
228 +import RankingCard from '@/components/RankingCard.vue'
235 import { useMediaPreview } from '@/composables/useMediaPreview'; 229 import { useMediaPreview } from '@/composables/useMediaPreview';
236 // 默认家庭封面图 230 // 默认家庭封面图
237 const defaultFamilyCover = 'https://cdn.ipadbiz.cn/lls_prog/images/default-family-cover.png'; 231 const defaultFamilyCover = 'https://cdn.ipadbiz.cn/lls_prog/images/default-family-cover.png';
......
1 /* 1 /*
2 * @Date: 2022-09-19 14:11:06 2 * @Date: 2022-09-19 14:11:06
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-08 14:00:36 4 + * @LastEditTime: 2025-09-09 10:52:43
5 * @FilePath: /lls_program/src/utils/config.js 5 * @FilePath: /lls_program/src/utils/config.js
6 * @Description: 环境配置文件 - 根据小程序运行环境自动切换API地址 6 * @Description: 环境配置文件 - 根据小程序运行环境自动切换API地址
7 */ 7 */
...@@ -46,7 +46,7 @@ export const DEFAULT_COVER_IMG = 'https://images.unsplash.com/photo-1558981806-e ...@@ -46,7 +46,7 @@ export const DEFAULT_COVER_IMG = 'https://images.unsplash.com/photo-1558981806-e
46 // 主题颜色配置 46 // 主题颜色配置
47 export const THEME_COLORS = { 47 export const THEME_COLORS = {
48 // 主题蓝色 - 可统一调整 48 // 主题蓝色 - 可统一调整
49 - PRIMARY: '#4A90E2', 49 + PRIMARY: '#54ABAE',
50 // 其他颜色可以在这里添加 50 // 其他颜色可以在这里添加
51 SECONDARY: '#6B7280', 51 SECONDARY: '#6B7280',
52 SUCCESS: '#10B981', 52 SUCCESS: '#10B981',
......