219 lines
7.7 KiB
TypeScript
219 lines
7.7 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
|
||
interface RankingUser {
|
||
username: string
|
||
count: number
|
||
}
|
||
|
||
interface RankingData {
|
||
contributors: RankingUser[]
|
||
downloads: RankingUser[]
|
||
likes: RankingUser[]
|
||
}
|
||
|
||
export default function RankingPanel() {
|
||
const [isVisible, setIsVisible] = useState(true)
|
||
const [currentTab, setCurrentTab] = useState<'contributors' | 'downloads' | 'likes'>('contributors')
|
||
const [rankings, setRankings] = useState<RankingData>({
|
||
contributors: [],
|
||
downloads: [],
|
||
likes: []
|
||
})
|
||
const [loading, setLoading] = useState(true)
|
||
|
||
// 自动轮播
|
||
useEffect(() => {
|
||
const tabs: ('contributors' | 'downloads' | 'likes')[] = ['contributors', 'downloads', 'likes']
|
||
let currentIndex = 0
|
||
|
||
const interval = setInterval(() => {
|
||
currentIndex = (currentIndex + 1) % tabs.length
|
||
setCurrentTab(tabs[currentIndex])
|
||
}, 5000) // 每5秒切换一次
|
||
|
||
return () => clearInterval(interval)
|
||
}, [])
|
||
|
||
// 获取排行榜数据
|
||
useEffect(() => {
|
||
const fetchRankings = async () => {
|
||
try {
|
||
const response = await fetch('/api/rankings')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setRankings(data)
|
||
}
|
||
} catch (error) {
|
||
console.error('获取排行榜失败:', error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
fetchRankings()
|
||
// 每30秒更新一次数据
|
||
const interval = setInterval(fetchRankings, 30000)
|
||
return () => clearInterval(interval)
|
||
}, [])
|
||
|
||
const handleClose = () => {
|
||
setIsVisible(false)
|
||
// 不再保存到localStorage,刷新后会重新显示
|
||
}
|
||
|
||
if (!isVisible) return null
|
||
|
||
const getMedalIcon = (index: number) => {
|
||
switch (index) {
|
||
case 0:
|
||
return (
|
||
<svg className="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
|
||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||
</svg>
|
||
)
|
||
case 1:
|
||
return (
|
||
<svg className="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||
</svg>
|
||
)
|
||
case 2:
|
||
return (
|
||
<svg className="w-5 h-5 text-orange-600" fill="currentColor" viewBox="0 0 20 20">
|
||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||
</svg>
|
||
)
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
const getTabTitle = () => {
|
||
switch (currentTab) {
|
||
case 'contributors':
|
||
return '🏆 贡献排行榜'
|
||
case 'downloads':
|
||
return '📥 下载排行榜'
|
||
case 'likes':
|
||
return '❤️ 点赞排行榜'
|
||
}
|
||
}
|
||
|
||
const getCurrentData = () => {
|
||
switch (currentTab) {
|
||
case 'contributors':
|
||
return rankings.contributors
|
||
case 'downloads':
|
||
return rankings.downloads
|
||
case 'likes':
|
||
return rankings.likes
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="fixed left-8 top-1/2 -translate-y-1/2 z-40 w-[150px] h-[400px] bg-gray-900 dark:bg-gray-950 rounded-lg shadow-2xl border border-gray-700 dark:border-gray-800 overflow-hidden backdrop-blur-sm">
|
||
{/* 关闭按钮 */}
|
||
<button
|
||
onClick={handleClose}
|
||
className="absolute top-1.5 right-1.5 w-3 h-3 flex items-center justify-center bg-gray-800 hover:bg-gray-700 rounded-full transition-colors z-10"
|
||
aria-label="关闭"
|
||
>
|
||
<svg className="w-2 h-2 text-gray-400 hover:text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
|
||
{/* 标题 */}
|
||
<div className="bg-gradient-to-r from-gray-800 to-gray-900 px-3 py-3 text-center border-b border-gray-700">
|
||
<h3 className="text-sm font-bold text-white leading-tight">
|
||
{getTabTitle()}
|
||
</h3>
|
||
</div>
|
||
|
||
{/* 指示器 */}
|
||
<div className="flex justify-center gap-1 py-2 bg-gray-800 border-b border-gray-700">
|
||
<button
|
||
onClick={() => setCurrentTab('contributors')}
|
||
className={`w-1.5 h-1.5 rounded-full transition-all ${
|
||
currentTab === 'contributors' ? 'bg-white w-4' : 'bg-gray-600'
|
||
}`}
|
||
/>
|
||
<button
|
||
onClick={() => setCurrentTab('downloads')}
|
||
className={`w-1.5 h-1.5 rounded-full transition-all ${
|
||
currentTab === 'downloads' ? 'bg-white w-4' : 'bg-gray-600'
|
||
}`}
|
||
/>
|
||
<button
|
||
onClick={() => setCurrentTab('likes')}
|
||
className={`w-1.5 h-1.5 rounded-full transition-all ${
|
||
currentTab === 'likes' ? 'bg-white w-4' : 'bg-gray-600'
|
||
}`}
|
||
/>
|
||
</div>
|
||
|
||
{/* 排行榜内容 */}
|
||
<div className="p-3 overflow-y-auto" style={{ height: 'calc(100% - 80px)' }}>
|
||
{loading ? (
|
||
<div className="flex items-center justify-center h-full">
|
||
<div className="text-xs text-gray-400">加载中...</div>
|
||
</div>
|
||
) : getCurrentData().length === 0 ? (
|
||
<div className="flex items-center justify-center h-full">
|
||
<div className="text-xs text-gray-500 text-center">暂无数据</div>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{getCurrentData().map((user, index) => (
|
||
<div
|
||
key={index}
|
||
className={`flex items-center gap-2 p-2 rounded-lg transition-all ${
|
||
index < 3
|
||
? 'bg-gradient-to-r from-yellow-900/30 to-orange-900/30 border border-yellow-600/30'
|
||
: 'bg-gray-800/50 border border-gray-700'
|
||
}`}
|
||
>
|
||
{/* 排名/奖牌 */}
|
||
<div className="flex-shrink-0 w-6 flex items-center justify-center">
|
||
{index < 3 ? (
|
||
getMedalIcon(index)
|
||
) : (
|
||
<span className="text-xs font-medium text-gray-400">
|
||
{index + 1}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* 用户名 */}
|
||
<div className="flex-1 min-w-0">
|
||
<div
|
||
className={`text-xs font-medium truncate ${
|
||
index < 3
|
||
? 'text-yellow-400'
|
||
: 'text-gray-200'
|
||
}`}
|
||
>
|
||
{user.username}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 数量 */}
|
||
<div className="flex-shrink-0">
|
||
<span className={`text-xs font-bold ${
|
||
index < 3 ? 'text-yellow-400' : 'text-gray-400'
|
||
}`}>
|
||
{user.count}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|