OfficeSystem/components/index/RankingBoard.vue
2025-11-13 13:51:52 +08:00

533 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="ranking-board">
<!-- 顶部Header -->
<view class="ranking-header">
<view class="header-content">
<text class="header-title">排行榜</text>
<view class="trophy-icon">🏆</view>
</view>
</view>
<!-- 内容区域 -->
<scroll-view class="ranking-content" scroll-y>
<!-- Tabs -->
<view class="ranking-tabs">
<view
class="tab-item"
:class="{ active: currentTab === 'today' }"
@click="switchTab('today')"
>
日排行
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'week' }"
@click="switchTab('week')"
>
周排行
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'month' }"
@click="switchTab('month')"
>
月排行
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-container">
<view class="loading-text">加载中...</view>
</view>
<!-- 排行榜内容 -->
<view v-else class="ranking-list-container">
<!-- 当前用户排名(高亮显示) -->
<view v-if="currentUserRank" class="my-ranking-card">
<view class="my-ranking-stats">
<view class="stat-item">
<text class="stat-value">我</text>
</view>
<view class="stat-item">
<text class="stat-value">第{{ currentUserRank.rank }}名</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ currentUserRank.addNum }}</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ currentUserRank.followNum }}</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ currentUserRank.intentionNum }}</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ currentUserRank.addWxNum }}</text>
</view>
</view>
</view>
<!-- 表头 -->
<view class="ranking-table-header">
<view class="header-col rank-col">排名</view>
<view class="header-col name-col">名称</view>
<view class="header-col stat-col">新增客户</view>
<view class="header-col stat-col">跟进客户</view>
<view class="header-col stat-col">意向客户</view>
<view class="header-col stat-col">加微信</view>
</view>
<!-- 排行榜列表 -->
<view class="ranking-list">
<view
v-for="(item, index) in currentRankingList"
:key="item.userId"
class="ranking-item"
:class="{ 'is-current-user': String(item.userId) === String(currentUserId) }"
>
<!-- 排名 -->
<view class="rank-col">
<view v-if="index === 0" class="medal gold">🥇</view>
<view v-else-if="index === 1" class="medal silver">🥈</view>
<view v-else-if="index === 2" class="medal bronze">🥉</view>
<text v-else class="rank-number">{{ index + 1 }}</text>
</view>
<!-- 名称 -->
<view class="name-col">
<text class="user-name">{{ item.userName }}</text>
</view>
<!-- 统计数据 -->
<view class="stat-col">
<text class="stat-text">{{ item.addNum }}</text>
</view>
<view class="stat-col">
<text class="stat-text">{{ item.followNum }}</text>
</view>
<view class="stat-col">
<text class="stat-text">{{ item.intentionNum }}</text>
</view>
<view class="stat-col">
<text class="stat-text">{{ item.addWxNum }}</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="!loading && currentRankingList.length === 0" class="empty-state">
<text class="empty-text">暂无数据</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { getCustomerStatistics } from '@/api/dashboard';
import { useUserStore } from '@/store/user';
// 当前选中的tab
const currentTab = ref('today');
// 加载状态
const loading = ref(false);
// 排行榜数据
const rankingData = ref({
today: [],
week: [],
month: []
});
// 当前用户ID
const userStore = useUserStore();
const currentUserId = computed(() => {
const userId = userStore.userInfo?.user.userId;
console.log('userId',userStore.userInfo.user.userId);
return String(userId); // 确保返回字符串类型,与接口数据一致
});
// 当前显示的排行榜列表(原始数据,不修改)
const currentRankingList = computed(() => {
return rankingData.value[currentTab.value] || [];
});
// 当前用户的排名信息(从排行榜数据中查找并提取,不修改原数据)
const currentUserRank = computed(() => {
const list = currentRankingList.value;
const userId = currentUserId.value;
// 如果没有用户ID返回null
if (!userId) return null;
// 在排行榜数据中查找自己的ID
const userItem = list.find(item => String(item.userId) === String(userId));
// 如果找不到返回null
if (!userItem) return null;
// 计算排名(索引+1
const rank = list.findIndex(item => String(item.userId) === String(userId)) + 1;
console.log('userrank',rank);
// 返回用户信息的副本,添加排名信息(不修改原数据)
return {
...userItem,
rank: rank
};
});
// 切换tab
const switchTab = (tab) => {
currentTab.value = tab;
};
// 获取头像文字(取姓名最后一个字)
const getAvatarText = (name) => {
if (!name) return '?';
return name.length > 1 ? name.slice(-1) : name;
};
// 加载排行榜数据
const loadRankingData = async () => {
try {
loading.value = true;
const res = await getCustomerStatistics();
console.log('排行榜数据:', res);
if (res ) {
rankingData.value = {
today: res.today || [],
week: res.week || [],
month: res.month || []
};
}
} catch (err) {
console.error('加载排行榜数据失败:', err);
uni.showToast({
title: '加载数据失败',
icon: 'none'
});
} finally {
loading.value = false;
}
};
// 组件挂载时加载数据
onMounted(() => {
loadRankingData();
});
</script>
<style lang="scss" scoped>
.ranking-board {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: #f6f7fb;
overflow: hidden;
position: relative;
}
.ranking-header {
background: linear-gradient(135deg, #ffd700 0%, #ffb347 100%);
padding: 40rpx 32rpx 60rpx;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 300rpx;
height: 300rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
transform: translate(30%, -30%);
}
&::after {
content: '';
position: absolute;
bottom: -50rpx;
right: -50rpx;
width: 200rpx;
height: 200rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 1;
}
.header-title {
font-size: 48rpx;
font-weight: bold;
color: #ffffff;
}
.trophy-icon {
font-size: 80rpx;
line-height: 1;
}
.ranking-content {
flex: 1;
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
margin-top: -32rpx;
position: relative;
z-index: 2;
padding-top: 24rpx;
height: 0; /* 配合 flex: 1 使用 */
}
.ranking-tabs {
display: flex;
padding: 0 32rpx;
border-bottom: 2rpx solid #f0f0f0;
margin-bottom: 24rpx;
}
.tab-item {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #666666;
position: relative;
&.active {
color: #ff6b35;
font-weight: 600;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background: #ff6b35;
border-radius: 2rpx;
}
}
}
.loading-container {
padding: 100rpx 0;
text-align: center;
}
.loading-text {
font-size: 28rpx;
color: #999999;
}
.ranking-list-container {
padding: 0 32rpx 40rpx;
}
.my-ranking-card {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-radius: 16rpx;
padding: 24rpx 0;
margin-bottom: 24rpx;
}
.my-ranking-header {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.my-label {
flex: 1;
font-size: 28rpx;
font-weight: 600;
color: #1976d2;
margin-right: 16rpx;
}
.my-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
}
.avatar-text {
font-size: 24rpx;
color: #1976d2;
font-weight: 600;
}
.my-rank {
flex: 1;
font-size: 28rpx;
font-weight: 600;
color: #1976d2;
margin-left: auto;
}
.my-ranking-stats {
display: flex;
justify-content: space-around;
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-value {
font-size: 32rpx;
font-weight: bold;
color: #1976d2;
}
.ranking-table-header {
display: flex;
padding: 16rpx 0;
background: #f8f9fa;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.header-col {
flex: 1;
font-size: 24rpx;
color: #666666;
text-align: center;
&.rank-col {
flex:1;
flex-shrink: 0;
}
&.name-col {
text-align: left;
justify-content: center;
}
&.stat-col {
flex-shrink: 0;
}
}
.ranking-list {
display: flex;
flex-direction: column;
}
.ranking-item {
display: flex;
align-items: center;
padding: 24rpx 0;
flex:1;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&.is-current-user {
background: #f0f7ff;
border-radius: 8rpx;
margin: 8rpx 0;
}
}
.rank-col {
flex:1;
display: flex;
align-items: center;
justify-content: center;
}
.medal {
font-size: 40rpx;
line-height: 1;
}
.rank-number {
font-size: 28rpx;
color: #333333;
font-weight: 600;
}
.name-col {
flex:1;
display: flex;
align-items: center;
}
.user-avatar {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
}
.user-avatar .avatar-text {
font-size: 22rpx;
color: #666666;
font-weight: 500;
}
.user-name {
font-size: 28rpx;
color: #333333;
}
.stat-col {
flex:1;
flex-shrink: 0;
text-align: center;
}
.stat-text {
font-size: 26rpx;
color: #333333;
}
.empty-state {
padding: 100rpx 0;
text-align: center;
}
.empty-text {
font-size: 28rpx;
color: #999999;
}
</style>