Skip to content

Commit e0f8026

Browse files
committed
新增功能 - 用户薪资排行榜
1 parent d43535e commit e0f8026

File tree

6 files changed

+483
-2
lines changed

6 files changed

+483
-2
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import request from '../utils/request'
2+
3+
// 获取薪资排行榜
4+
export const getSalaryRanking = (limit = 30) => {
5+
return request.get('/ranking/salary', {
6+
params: { limit }
7+
})
8+
}

coder-test-frontend/src/views/Home.vue

Lines changed: 337 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,77 @@
7171
</el-card>
7272
</div>
7373
</div>
74+
75+
<!-- 薪资排行榜 -->
76+
<div class="ranking-section">
77+
<div class="section-header">
78+
<h2 class="section-title">薪资排行榜</h2>
79+
<div class="section-subtitle">群雄逐鹿 · 谁主沉浮</div>
80+
</div>
81+
82+
<div v-loading="rankingLoading" class="ranking-content">
83+
<div v-if="rankingList.length === 0 && !rankingLoading" class="no-ranking">
84+
<el-empty description="暂无排行榜数据" />
85+
</div>
86+
87+
<div v-else class="ranking-grid">
88+
<div
89+
v-for="(column, columnIndex) in rankingColumns"
90+
:key="columnIndex"
91+
class="ranking-column"
92+
>
93+
<div class="column-header">
94+
<span class="column-title">第{{ columnIndex * 10 + 1 }}-{{ Math.min((columnIndex + 1) * 10, rankingList.length) }}名</span>
95+
</div>
96+
97+
<div class="ranking-items">
98+
<div
99+
v-for="user in column"
100+
:key="user.id"
101+
class="ranking-item"
102+
:class="getRankingClass(user.rank)"
103+
>
104+
<div class="ranking-number">{{ user.rank }}</div>
105+
<el-avatar
106+
:src="user.avatar"
107+
:size="40"
108+
class="ranking-avatar"
109+
>
110+
<el-icon><User /></el-icon>
111+
</el-avatar>
112+
<div class="ranking-info">
113+
<div class="ranking-name">{{ user.nickname }}</div>
114+
<div class="ranking-salary">¥{{ user.salary?.toLocaleString() || 0 }}/月</div>
115+
</div>
116+
<div v-if="user.rank <= 3" class="ranking-medal">
117+
<el-icon v-if="user.rank === 1" class="medal gold"><Trophy /></el-icon>
118+
<el-icon v-else-if="user.rank === 2" class="medal silver"><Trophy /></el-icon>
119+
<el-icon v-else-if="user.rank === 3" class="medal bronze"><Trophy /></el-icon>
120+
</div>
121+
</div>
122+
</div>
123+
</div>
124+
</div>
125+
</div>
126+
</div>
74127
</div>
75128
</div>
76129
</template>
77130

78131
<script setup>
79-
import { computed } from 'vue'
132+
import { computed, ref, onMounted } from 'vue'
80133
import { useRouter } from 'vue-router'
81134
import { useUserStore } from '../stores/user'
135+
import { getSalaryRanking } from '../api/ranking'
136+
import { ElMessage } from 'element-plus'
82137
import {
83138
KnifeFork,
84139
Coin,
85140
MagicStick,
86141
TrendCharts,
87-
Guide
142+
Guide,
143+
User,
144+
Trophy
88145
} from '@element-plus/icons-vue'
89146
import bannerImage from '../assets/banner.png'
90147
import GlobalNavbar from '../components/GlobalNavbar.vue'
@@ -95,6 +152,45 @@ const userStore = useUserStore()
95152
const isLoggedIn = computed(() => userStore.isLoggedIn)
96153
const user = computed(() => userStore.user)
97154
155+
// 排行榜相关数据
156+
const rankingList = ref([])
157+
const rankingLoading = ref(false)
158+
159+
// 计算排行榜分列显示
160+
const rankingColumns = computed(() => {
161+
const columns = []
162+
for (let i = 0; i < 3; i++) {
163+
const start = i * 10
164+
const end = Math.min(start + 10, rankingList.value.length)
165+
if (start < rankingList.value.length) {
166+
columns.push(rankingList.value.slice(start, end))
167+
}
168+
}
169+
return columns
170+
})
171+
172+
// 获取排名样式类
173+
const getRankingClass = (rank) => {
174+
if (rank === 1) return 'rank-first'
175+
if (rank === 2) return 'rank-second'
176+
if (rank === 3) return 'rank-third'
177+
return ''
178+
}
179+
180+
// 加载排行榜数据
181+
const loadRanking = async () => {
182+
rankingLoading.value = true
183+
try {
184+
const data = await getSalaryRanking(30)
185+
rankingList.value = data || []
186+
} catch (error) {
187+
console.error('获取排行榜失败:', error)
188+
ElMessage.error('获取排行榜失败')
189+
} finally {
190+
rankingLoading.value = false
191+
}
192+
}
193+
98194
const handleLogout = async () => {
99195
await userStore.logoutUser()
100196
router.push('/')
@@ -113,6 +209,11 @@ const handleChallengeClick = () => {
113209
router.push('/login')
114210
}
115211
}
212+
213+
// 组件挂载时加载排行榜
214+
onMounted(() => {
215+
loadRanking()
216+
})
116217
</script>
117218

118219
<style scoped>
@@ -380,6 +481,214 @@ const handleChallengeClick = () => {
380481
opacity: 0.2;
381482
}
382483
484+
/* 排行榜样式 */
485+
.ranking-section {
486+
margin-top: 80px;
487+
}
488+
489+
.ranking-content {
490+
min-height: 300px;
491+
}
492+
493+
.no-ranking {
494+
display: flex;
495+
justify-content: center;
496+
align-items: center;
497+
height: 300px;
498+
}
499+
500+
.ranking-grid {
501+
display: grid;
502+
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
503+
gap: 30px;
504+
margin-top: 40px;
505+
}
506+
507+
.ranking-column {
508+
background: var(--bg-card);
509+
border: 2px solid var(--border-light);
510+
border-radius: 16px;
511+
overflow: hidden;
512+
box-shadow: 0 8px 24px var(--shadow-medium);
513+
transition: all 0.3s ease;
514+
}
515+
516+
.ranking-column:hover {
517+
transform: translateY(-4px);
518+
box-shadow: 0 12px 32px var(--shadow-heavy);
519+
border-color: var(--accent-gold);
520+
}
521+
522+
.column-header {
523+
background: linear-gradient(135deg, var(--primary-brown) 0%, var(--secondary-brown) 100%);
524+
color: var(--bg-card);
525+
padding: 16px 20px;
526+
text-align: center;
527+
position: relative;
528+
}
529+
530+
.column-header::after {
531+
content: '';
532+
position: absolute;
533+
bottom: 0;
534+
left: 0;
535+
right: 0;
536+
height: 3px;
537+
background: linear-gradient(90deg, var(--accent-gold) 0%, var(--accent-copper) 100%);
538+
}
539+
540+
.column-title {
541+
font-size: 16px;
542+
font-weight: 600;
543+
letter-spacing: 1px;
544+
}
545+
546+
.ranking-items {
547+
padding: 20px;
548+
}
549+
550+
.ranking-item {
551+
display: flex;
552+
align-items: center;
553+
padding: 12px 16px;
554+
margin-bottom: 12px;
555+
background: var(--bg-secondary);
556+
border: 1px solid var(--border-light);
557+
border-radius: 12px;
558+
transition: all 0.3s ease;
559+
position: relative;
560+
overflow: hidden;
561+
}
562+
563+
.ranking-item::before {
564+
content: '';
565+
position: absolute;
566+
top: 0;
567+
left: -100%;
568+
width: 100%;
569+
height: 100%;
570+
background: linear-gradient(90deg, transparent, rgba(218, 165, 32, 0.1), transparent);
571+
transition: left 0.6s ease;
572+
}
573+
574+
.ranking-item:hover::before {
575+
left: 100%;
576+
}
577+
578+
.ranking-item:hover {
579+
transform: translateX(4px);
580+
border-color: var(--accent-gold);
581+
box-shadow: 0 4px 12px var(--shadow-light);
582+
}
583+
584+
.ranking-item:last-child {
585+
margin-bottom: 0;
586+
}
587+
588+
.ranking-item.rank-first {
589+
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, rgba(255, 193, 7, 0.05) 100%);
590+
border-color: #FFD700;
591+
}
592+
593+
.ranking-item.rank-second {
594+
background: linear-gradient(135deg, rgba(192, 192, 192, 0.1) 0%, rgba(169, 169, 169, 0.05) 100%);
595+
border-color: #C0C0C0;
596+
}
597+
598+
.ranking-item.rank-third {
599+
background: linear-gradient(135deg, rgba(205, 127, 50, 0.1) 0%, rgba(184, 115, 51, 0.05) 100%);
600+
border-color: #CD7F32;
601+
}
602+
603+
.ranking-number {
604+
width: 32px;
605+
height: 32px;
606+
background: var(--primary-brown);
607+
color: var(--bg-card);
608+
border-radius: 50%;
609+
display: flex;
610+
align-items: center;
611+
justify-content: center;
612+
font-weight: 600;
613+
font-size: 14px;
614+
margin-right: 12px;
615+
flex-shrink: 0;
616+
}
617+
618+
.rank-first .ranking-number {
619+
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
620+
color: #333;
621+
}
622+
623+
.rank-second .ranking-number {
624+
background: linear-gradient(135deg, #C0C0C0 0%, #A9A9A9 100%);
625+
color: #333;
626+
}
627+
628+
.rank-third .ranking-number {
629+
background: linear-gradient(135deg, #CD7F32 0%, #B8733B 100%);
630+
color: #fff;
631+
}
632+
633+
.ranking-avatar {
634+
margin-right: 12px;
635+
border: 2px solid var(--border-light);
636+
flex-shrink: 0;
637+
}
638+
639+
.rank-first .ranking-avatar {
640+
border-color: #FFD700;
641+
}
642+
643+
.rank-second .ranking-avatar {
644+
border-color: #C0C0C0;
645+
}
646+
647+
.rank-third .ranking-avatar {
648+
border-color: #CD7F32;
649+
}
650+
651+
.ranking-info {
652+
flex: 1;
653+
min-width: 0;
654+
}
655+
656+
.ranking-name {
657+
font-weight: 600;
658+
color: var(--text-primary);
659+
margin-bottom: 4px;
660+
overflow: hidden;
661+
text-overflow: ellipsis;
662+
white-space: nowrap;
663+
}
664+
665+
.ranking-salary {
666+
color: var(--accent-gold);
667+
font-weight: 600;
668+
font-size: 14px;
669+
}
670+
671+
.ranking-medal {
672+
margin-left: 8px;
673+
flex-shrink: 0;
674+
}
675+
676+
.medal {
677+
font-size: 20px;
678+
}
679+
680+
.medal.gold {
681+
color: #FFD700;
682+
}
683+
684+
.medal.silver {
685+
color: #C0C0C0;
686+
}
687+
688+
.medal.bronze {
689+
color: #CD7F32;
690+
}
691+
383692
/* 响应式设计 */
384693
@media (max-width: 768px) {
385694
@@ -396,6 +705,32 @@ const handleChallengeClick = () => {
396705
gap: 30px;
397706
}
398707
708+
.ranking-grid {
709+
grid-template-columns: 1fr;
710+
gap: 20px;
711+
}
712+
713+
.ranking-column {
714+
margin-bottom: 20px;
715+
}
716+
717+
.ranking-item {
718+
padding: 10px 12px;
719+
}
720+
721+
.ranking-avatar {
722+
width: 36px !important;
723+
height: 36px !important;
724+
}
725+
726+
.ranking-name {
727+
font-size: 14px;
728+
}
729+
730+
.ranking-salary {
731+
font-size: 13px;
732+
}
733+
399734
.main-content {
400735
padding: 60px 20px;
401736
}

0 commit comments

Comments
 (0)