hookehuyr

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

- 新增 RankingCard 组件用于展示活动步数排行榜
- 将主题蓝色从 #4A90E2 更新为 #54ABAE
- 替换 Dashboard 页面的排行榜视图为新的 RankingCard 组件
......@@ -28,6 +28,7 @@ declare module 'vue' {
PointsCollector: typeof import('./src/components/PointsCollector.vue')['default']
PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
PrimaryButton: typeof import('./src/components/PrimaryButton.vue')['default']
RankingCard: typeof import('./src/components/RankingCard.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ShareButton: typeof import('./src/components/ShareButton/index.vue')['default']
......
......@@ -3,13 +3,13 @@
@tailwind utilities;
.bg-blue-500 {
background-color: #4A90E2 !important;
background-color: #54ABAE !important;
}
.text-blue-500 {
color: #4A90E2 !important;
color: #54ABAE !important;
}
.border-blue-500 {
border-color: #4A90E2 !important;
border-color: #54ABAE !important;
}
......
<!--
* @Date: 2025-01-09 00:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-08 20:22:19
* @FilePath: /lls_program/src/components/RankingCard.vue
* @Description: 排行榜卡片组件
-->
<template>
<view class="ranking-card">
<!-- 卡片头部 -->
<view class="card-header">
<view class="card-title">全市排行</view>
<view class="view-more" @tap="handleViewMore">查看更多</view>
</view>
<!-- 顶部导航 -->
<view class="nav-tabs">
<!-- 滑动指示器 -->
<view
class="tab-indicator"
:class="{ 'indicator-shanghai': activeTab === 'shanghai' }"
></view>
<view
class="tab-item"
:class="{ active: activeTab === 'huangpu' }"
@click="switchTab('huangpu')"
>
黄埔榜
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'shanghai' }"
@click="switchTab('shanghai')"
>
上海榜
</view>
</view>
<!-- 排行榜内容 -->
<view class="rank-content" :class="{ 'content-switching': isContentSwitching }">
<!-- 前三名展示 -->
<view class="top-three">
<!-- 第二名 -->
<view class="rank-item second">
<view class="crown crown-silver">👑</view>
<view class="avatar">
<image :src="topRanks[1]?.avatar" class="avatar-img" mode="aspectFill" />
</view>
<view class="family-name">{{ topRanks[1]?.familyName }}</view>
<view class="leader-name">大家长:{{ topRanks[1]?.leaderName }}</view>
<view class="rank-number">
<view class="rank-num">2</view>
<view class="steps-in-rank">{{ formatSteps(topRanks[1]?.steps) }}</view>
</view>
</view>
<!-- 第一名 -->
<view class="rank-item first">
<view class="crown crown-gold">👑</view>
<view class="avatar">
<image :src="topRanks[0]?.avatar" class="avatar-img" mode="aspectFill" />
</view>
<view class="family-name">{{ topRanks[0]?.familyName }}</view>
<view class="leader-name">大家长:{{ topRanks[0]?.leaderName }}</view>
<view class="rank-number">
<view class="rank-num">1</view>
<view class="steps-in-rank">{{ formatSteps(topRanks[0]?.steps) }}</view>
</view>
</view>
<!-- 第三名 -->
<view class="rank-item third">
<view class="crown crown-bronze">👑</view>
<view class="avatar">
<image :src="topRanks[2]?.avatar" class="avatar-img" mode="aspectFill" />
</view>
<view class="family-name">{{ topRanks[2]?.familyName }}</view>
<view class="leader-name">大家长:{{ topRanks[2]?.leaderName }}</view>
<view class="rank-number">
<view class="rank-num">3</view>
<view class="steps-in-rank">{{ formatSteps(topRanks[2]?.steps) }}</view>
</view>
</view>
</view>
</view>
<!-- 我的排名卡片 -->
<view class="my-rank-section">
<view class="my-rank-content">
<view class="my-rank-left">
<view class="my-rank-number">{{ myRank.rank }}+</view>
<view class="my-avatar">
<image :src="myRank.avatar" class="my-avatar-img" mode="aspectFill" />
</view>
<view class="my-family-info">
<view class="my-family-name">{{ myRank.familyName }}</view>
<view class="my-leader-name">大家长:{{ myRank.leaderName }}</view>
</view>
</view>
<view class="my-rank-right">
<view class="my-steps">{{ formatStepsForList(myRank.steps) }}</view>
<view class="rank-status">{{ myRank.status }}</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
// 定义props
const props = defineProps({
onViewMore: {
type: Function,
default: () => {}
}
})
// 当前激活的tab
const activeTab = ref('huangpu')
// 内容切换状态
const isContentSwitching = ref(false)
/**
* 切换tab
* @param {string} tab - tab名称
*/
const switchTab = (tab) => {
if (activeTab.value === tab) return
// 开始切换动画
isContentSwitching.value = true
// 延迟切换内容,让淡出动画先执行
setTimeout(() => {
activeTab.value = tab
// 内容切换后,结束切换状态,开始淡入动画
setTimeout(() => {
isContentSwitching.value = false
}, 50)
}, 200)
}
/**
* 格式化步数显示
* @param {number} steps - 步数
* @returns {string} 格式化后的步数
*/
const formatSteps = (steps) => {
return steps ? steps.toLocaleString() : '0'
}
/**
* 格式化步数显示(用于列表显示,使用千位分隔符格式)
* @param {number} steps - 步数
* @returns {string} 格式化后的步数
*/
const formatStepsForList = (steps) => {
return steps ? steps.toLocaleString() : '0'
}
/**
* 处理查看更多点击事件
*/
const handleViewMore = () => {
if (props.onViewMore) {
props.onViewMore()
}
}
// 前三名数据
const topRanks = ref([
{
rank: 1,
familyName: '明媚的晴',
leaderName: '张明',
avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
steps: 45670
},
{
rank: 2,
familyName: '甜心小桃',
leaderName: '李桃',
avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
steps: 42350
},
{
rank: 3,
familyName: '真心找爱',
leaderName: '王真',
avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
steps: 38920
}
])
// 我的排名信息
const myRank = ref({
rank: 99,
familyName: '和谐之家',
leaderName: '陈家明',
avatar: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
steps: 8920,
status: '未上榜'
})
</script>
<style lang="less">
.ranking-card {
background: linear-gradient(180deg, #4A90E2 0%, #357ABD 100%);
border-radius: 20rpx;
padding: 40rpx;
margin: 30rpx;
margin-bottom: 0;
position: relative;
overflow: hidden;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.card-title {
color: white;
font-size: 32rpx;
font-weight: 600;
}
.view-more {
color: rgba(255, 255, 255, 0.8);
font-size: 24rpx;
padding: 8rpx 16rpx;
border: 1rpx solid rgba(255, 255, 255, 0.3);
border-radius: 20rpx;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
}
}
.nav-tabs {
display: flex;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 60rpx;
padding: 8rpx;
margin-bottom: 40rpx;
position: relative;
overflow: hidden;
.tab-indicator {
position: absolute;
top: 8rpx;
left: 8rpx;
width: calc(50% - 8rpx);
height: calc(100% - 16rpx);
background: rgba(255, 255, 255, 1);
border-radius: 52rpx;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
z-index: 1;
&.indicator-shanghai {
transform: translateX(100%);
}
}
.tab-item {
flex: 1;
padding: 20rpx 0;
text-align: center;
border-radius: 52rpx;
color: rgba(255, 255, 255, 0.8);
font-size: 28rpx;
font-weight: 500;
transition: all 0.3s ease;
position: relative;
z-index: 2;
cursor: pointer;
&.active {
color: #4A90E2;
font-weight: 600;
transform: scale(1.02);
}
&:hover {
color: rgba(255, 255, 255, 1);
}
}
}
.rank-content {
transition: all 0.3s ease;
transform: translateY(0);
opacity: 1;
&.content-switching {
opacity: 0.3;
transform: translateY(-20rpx);
}
}
.top-three {
position: relative;
display: flex;
justify-content: center;
align-items: flex-end;
margin-bottom: 40rpx;
// height: 280rpx;
gap: -20rpx;
.rank-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
.crown {
font-size: 30rpx;
margin-bottom: 8rpx;
&.crown-gold {
filter: grayscale(0.3) brightness(1.1);
}
&.crown-silver {
filter: hue-rotate(45deg) brightness(1.2);
}
&.crown-bronze {
filter: hue-rotate(25deg) brightness(0.9);
}
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
border: 4rpx solid rgba(255, 255, 255, 0.8);
margin-bottom: 12rpx;
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.family-name {
color: white;
font-size: 20rpx;
font-weight: 600;
margin-bottom: 6rpx;
text-align: center;
}
.leader-name {
color: rgba(255, 255, 255, 0.8);
font-size: 16rpx;
margin-bottom: 12rpx;
text-align: center;
}
.rank-number {
width: 100rpx;
height: 120rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
border-radius: 16rpx 16rpx 0 0;
box-shadow: 0 6rpx 12rpx rgba(0, 0, 0, 0.2);
.rank-num {
font-size: 32rpx;
font-weight: bold;
color: white;
margin-bottom: 8rpx;
}
.steps-in-rank {
font-size: 22rpx;
font-weight: 600;
color: white;
text-align: center;
line-height: 1.2;
}
}
&.second {
order: 1;
margin-top: 40rpx;
margin-right: -10rpx;
z-index: 2;
.avatar {
box-shadow: 0 0 10rpx rgba(192, 192, 192, 0.4);
}
.rank-number {
width: 120rpx;
height: 110rpx;
background: linear-gradient(135deg, #C0C0C0 0%, #A0A0A0 100%);
}
}
&.first {
order: 2;
z-index: 3;
.avatar {
width: 100rpx;
height: 100rpx;
border-color: #FFD700;
box-shadow: 0 0 15rpx rgba(255, 215, 0, 0.5);
}
.rank-number {
width: 120rpx;
height: 140rpx;
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
}
}
&.third {
order: 3;
margin-top: 40rpx;
margin-left: -10rpx;
z-index: 1;
.avatar {
box-shadow: 0 0 10rpx rgba(205, 127, 50, 0.4);
}
.rank-number {
width: 120rpx;
height: 100rpx;
background: linear-gradient(135deg, #CD7F32 0%, #B8860B 100%);
}
}
}
}
.my-rank-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 16rpx;
padding: 20rpx 24rpx;
backdrop-filter: blur(10px);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
.my-rank-content {
display: flex;
align-items: center;
justify-content: space-between;
.my-rank-left {
display: flex;
align-items: center;
flex: 1;
.my-rank-number {
width: 48rpx;
height: 48rpx;
background: #4A90E2;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
font-weight: bold;
color: white;
margin-right: 20rpx;
}
.my-avatar {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 20rpx;
.my-avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.my-family-info {
flex: 1;
.my-family-name {
font-size: 24rpx;
font-weight: 600;
color: #333;
margin-bottom: 6rpx;
}
.my-leader-name {
font-size: 20rpx;
color: #666;
}
}
}
.my-rank-right {
text-align: right;
.my-steps {
font-size: 26rpx;
font-weight: bold;
color: #4A90E2;
margin-bottom: 6rpx;
}
.rank-status {
font-size: 20rpx;
color: #999;
}
}
}
}
}
</style>
......@@ -132,15 +132,8 @@
</template>
</WeRunAuth>
<!-- 活动排行榜 -->
<view class="px-5 mb-4 mt-4">
<view @tap="openFamilyRank" class="w-full bg-blue-500 text-white py-3 rounded-lg flex flex-col items-center justify-center">
<view class="flex items-center justify-center">
<Category size="16" class="mr-2" />
昨日活动步数排行榜
</view>
</view>
</view>
<!-- 排行榜卡片 -->
<RankingCard :onViewMore="openFamilyRank" />
<!-- Family album -->
<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';
import TotalPointsDisplay from '@/components/TotalPointsDisplay.vue';
import PointsCollector from '@/components/PointsCollector.vue'
import WeRunAuth from '@/components/WeRunAuth.vue'
import RankingCard from '@/components/RankingCard.vue'
import { useMediaPreview } from '@/composables/useMediaPreview';
// 默认家庭封面图
const defaultFamilyCover = 'https://cdn.ipadbiz.cn/lls_prog/images/default-family-cover.png';
......
/*
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-08 14:00:36
* @LastEditTime: 2025-09-09 10:52:43
* @FilePath: /lls_program/src/utils/config.js
* @Description: 环境配置文件 - 根据小程序运行环境自动切换API地址
*/
......@@ -46,7 +46,7 @@ export const DEFAULT_COVER_IMG = 'https://images.unsplash.com/photo-1558981806-e
// 主题颜色配置
export const THEME_COLORS = {
// 主题蓝色 - 可统一调整
PRIMARY: '#4A90E2',
PRIMARY: '#54ABAE',
// 其他颜色可以在这里添加
SECONDARY: '#6B7280',
SUCCESS: '#10B981',
......