Files
ai-dimension/client/src/pages/leaderboard/leaderboard.vue
T

321 lines
6.8 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="page-leaderboard page-layout">
<view class="status-bar-placeholder"></view>
<!-- 顶部导航 -->
<view class="header">
<text class="title">🏅 排行榜</text>
</view>
<!-- 维度切换 -->
<view class="dim-tabs">
<view
v-for="tab in dimTabs"
:key="tab.value"
class="dim-tab"
:class="{ active: activeDim === tab.value }"
@tap="activeDim = tab.value; loadBoard()"
>
<text>{{ tab.label }}</text>
</view>
</view>
<!-- 排名列表 -->
<view class="board-list" v-if="board.length > 0">
<view
v-for="(item, idx) in board"
:key="item.userId || item.username || idx"
class="board-item"
:class="{ current: item.isCurrentUser }"
>
<!-- 排名 -->
<view class="rank">
<text v-if="idx < 3" class="rank-top">{{ idx + 1 }}</text>
<text v-else class="rank-num">{{ idx + 1 }}</text>
</view>
<!-- 头像 -->
<view class="avatar">
<image
v-if="item.avatarUrl"
:src="item.avatarUrl"
class="avatar-img"
mode="aspectFill"
/>
<text v-else class="avatar-text">
{{ (item.nickName || item.username || '用').charAt(0) }}
</text>
</view>
<!-- 信息 -->
<view class="user-info">
<text class="user-name">{{ item.nickName || item.username || '用户' }}</text>
<text class="user-score">{{ item.score || 0 }} </text>
</view>
<!-- 已解锁维度 -->
<view class="unlocked-dims" v-if="item.unlockedDims">
<text
v-for="(dimId, dIdx) in item.unlockedDims"
:key="dIdx"
class="dim-dot"
:style="{ background: dimDotColor(dimId) }"
></text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else-if="!loading">
<text class="empty-icon">🏅</text>
<text class="empty-text">还没有排行数据</text>
<text class="empty-sub">努力探索看看你能排第几</text>
</view>
<!-- 加载状态 -->
<view class="empty-state" v-else>
<text class="empty-icon" style="opacity:0.4"></text>
<text class="empty-text">加载中...</text>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { userApi } from '@/utils/api'
import { getDimension } from '@/utils/dimensions'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const board = ref([])
const loading = ref(true)
const activeDim = ref('all')
const dimTabs = [
{ value: 'all', label: '全部' },
{ value: 1, label: 'AI 起源' },
{ value: 2, label: 'AI 发展' },
{ value: 3, label: 'AI 当前' },
{ value: 4, label: 'AI 学习' },
{ value: 5, label: 'AI 趋势' }
]
onMounted(() => {
if (!userStore.isLoggedIn) {
uni.showToast({ title: '请先登录', icon: 'none' })
uni.navigateTo({ url: '/pages/login/login' })
return
}
loadBoard()
})
async function loadBoard() {
loading.value = true
try {
const dimParam = activeDim.value === 'all' ? null : activeDim.value
const data = await userApi.leaderboard(dimParam)
const list = Array.isArray(data) ? data : data.list || []
// 标记当前用户
const myId = userStore.userInfo?.id || userStore.userInfo?.userId || ''
board.value = list.map((item) => ({
...item,
isCurrentUser: String(item.userId || item.id || '') === String(myId)
}))
} catch (e) {
console.error('[leaderboard] load failed:', e)
board.value = []
} finally {
loading.value = false
}
}
function dimDotColor(id) {
const dim = getDimension(id)
return dim ? dim.color : 'rgba(255,255,255,0.2)'
}
</script>
<style lang="scss" scoped>
.page-leaderboard {
min-height: 100vh;
padding: 0 20px 40px;
background: var(--bg-primary);
}
.status-bar-placeholder { height: 44px; }
// 顶部导航
.header {
padding: 12px 0 16px;
}
.title {
font-size: 22px;
font-weight: 600;
color: var(--text-primary);
}
// 维度切换
.dim-tabs {
display: flex;
gap: 8px;
overflow-x: auto;
padding: 0 0 14px;
-webkit-overflow-scrolling: touch;
}
.dim-tab {
flex-shrink: 0;
padding: 7px 16px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--glass-border);
font-size: 13px;
color: var(--text-tertiary);
transition: all 0.2s ease;
}
.dim-tab.active {
background: linear-gradient(135deg, rgba(179, 136, 255, 0.15), rgba(77, 182, 172, 0.15));
border-color: rgba(179, 136, 255, 0.3);
color: var(--text-primary);
font-weight: 500;
}
// 排名列表
.board-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.board-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
}
.board-item.current {
background: rgba(179, 136, 255, 0.08);
border-color: rgba(179, 136, 255, 0.25);
box-shadow: 0 0 24px rgba(179, 136, 255, 0.08);
}
// 排名
.rank {
width: 32px;
text-align: center;
flex-shrink: 0;
}
.rank-top {
font-size: 20px;
font-weight: 700;
background: linear-gradient(135deg, var(--dim1-color), var(--dim4-color));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.rank-num {
font-size: 14px;
color: var(--text-muted);
font-weight: 500;
}
// 头像
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(124, 77, 255, 0.3), rgba(0, 188, 212, 0.15));
border: 1px solid rgba(124, 77, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
}
.avatar-img {
width: 100%;
height: 100%;
}
.avatar-text {
font-size: 16px;
font-weight: 600;
color: #fff;
}
// 用户信息
.user-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.user-name {
font-size: 14px;
color: var(--text-primary);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-score {
font-size: 12px;
color: var(--text-tertiary);
}
// 已解锁维度
.unlocked-dims {
display: flex;
gap: 5px;
flex-shrink: 0;
}
.dim-dot {
width: 10px;
height: 10px;
border-radius: 50%;
opacity: 0.8;
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 0;
text-align: center;
}
.empty-icon {
font-size: 56px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 16px;
color: var(--text-tertiary);
display: block;
margin-bottom: 6px;
}
.empty-sub {
font-size: 13px;
color: var(--text-muted);
display: block;
}
</style>