/** * RankingPanel 组件 * * 排行榜面板组件 * 显示贡献者、下载、点赞排行榜 * 支持自动轮播、拖拽、折叠等功能 * 使用三层架构样式系统,支持深浅色主题 * * @created 2025-01-XX * @component */ 'use client' import { useState, useEffect, useRef, useMemo } from 'react' import Link from 'next/link' import { useThemeStyles, cn } from '@/lib/ui-styles' interface RankingUser { username: string count: number } interface RankingData { contributors: RankingUser[] downloads: RankingUser[] likes: RankingUser[] } type PanelTab = 'contributors' | 'downloads' | 'likes' const PANEL_DIMENSIONS = { width: 150, height: 400 } const BUTTON_SIZE = 48 const clampToViewport = ( position: { x: number; y: number }, size: { width: number; height: number } ) => { if (typeof window === 'undefined') return position const maxX = Math.max(0, window.innerWidth - size.width) const maxY = Math.max(0, window.innerHeight - size.height) return { x: Math.min(Math.max(0, position.x), maxX), y: Math.min(Math.max(0, position.y), maxY), } } const TAB_META: Record< PanelTab, { label: string; accent: string } > = { contributors: { label: '贡献榜', accent: 'text-amber-600', }, downloads: { label: '下载榜', accent: 'text-sky-600', }, likes: { label: '点赞榜', accent: 'text-rose-600', }, } export default function RankingPanel() { const styles = useThemeStyles() const [isVisible, setIsVisible] = useState(true) const [currentTab, setCurrentTab] = useState('contributors') const [rankings, setRankings] = useState({ contributors: [], downloads: [], likes: [] }) const [loading, setLoading] = useState(true) const [position, setPosition] = useState({ x: 32, y: 0 }) // 默认位置 left-8 = 32px const [panelCentered, setPanelCentered] = useState(true) const [isDragging, setIsDragging] = useState(false) const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) const panelRef = useRef(null) const buttonRef = useRef(null) const [buttonPosition, setButtonPosition] = useState({ x: 32, y: 0 }) // 缩略按钮位置 const [buttonCentered, setButtonCentered] = useState(true) const [isButtonDragging, setIsButtonDragging] = useState(false) const [buttonDragOffset, setButtonDragOffset] = useState({ x: 0, y: 0 }) // 自动轮播 - 滚动间隔调慢到10秒 useEffect(() => { const tabs: PanelTab[] = ['contributors', 'downloads', 'likes'] let currentIndex = 0 const interval = setInterval(() => { currentIndex = (currentIndex + 1) % tabs.length setCurrentTab(tabs[currentIndex]) }, 10000) // 每10秒切换一次(原来5秒,现在更慢) return () => clearInterval(interval) }, []) // 获取排行榜数据 useEffect(() => { const fetchRankings = async () => { try { const response = await fetch('/api/rankings') if (response.ok) { const data = await response.json() setRankings({ contributors: (data.contributors || []).filter((item: RankingUser) => item.count > 0), downloads: (data.downloads || []).filter((item: RankingUser) => item.count > 0), likes: (data.likes || []).filter((item: RankingUser) => item.count > 0), }) } } catch (error) { console.error('获取排行榜失败:', error) } finally { setLoading(false) } } fetchRankings() // 每30秒更新一次数据 const interval = setInterval(fetchRankings, 30000) return () => clearInterval(interval) }, []) const clampPanelPosition = (value: { x: number; y: number }) => { const width = panelRef.current?.offsetWidth || PANEL_DIMENSIONS.width const height = panelRef.current?.offsetHeight || PANEL_DIMENSIONS.height return clampToViewport(value, { width, height }) } const clampButtonPosition = (value: { x: number; y: number }) => { const width = buttonRef.current?.offsetWidth || BUTTON_SIZE const height = buttonRef.current?.offsetHeight || BUTTON_SIZE return clampToViewport(value, { width, height }) } const handleClose = () => { setIsVisible(false) // 保存关闭状态到localStorage localStorage.setItem('rankingPanelClosed', 'true') // 保存当前面板位置作为缩略按钮的初始位置 // 如果面板是居中模式,按钮也应该保持居中 if (panelCentered) { setButtonPosition({ x: position.x, y: 0 }) setButtonCentered(true) localStorage.setItem('rankingPanelButtonPosition', JSON.stringify({ x: position.x, y: 0, centered: true })) } else { const clamped = clampPanelPosition(position) setButtonPosition(clamped) setButtonCentered(false) localStorage.setItem('rankingPanelButtonPosition', JSON.stringify({ ...clamped, centered: false })) } } const handleOpen = () => { setIsVisible(true) localStorage.removeItem('rankingPanelClosed') setPosition((prev) => { const fallback = buttonCentered ? { x: buttonPosition.x, y: 0 } : buttonPosition.x === 0 && buttonPosition.y === 0 ? prev : buttonPosition const nextPosition = clampPanelPosition(fallback) setPanelCentered(buttonCentered && nextPosition.y === 0) localStorage.setItem('rankingPanelPosition', JSON.stringify(nextPosition)) return nextPosition }) } // 检查localStorage中的关闭状态 useEffect(() => { const closed = localStorage.getItem('rankingPanelClosed') if (closed === 'true') { setIsVisible(false) } }, []) // 拖动处理 const handleMouseDown = (e: React.MouseEvent) => { if (panelRef.current) { const rect = panelRef.current.getBoundingClientRect() setDragOffset({ x: e.clientX - rect.left, y: e.clientY - rect.top }) setIsDragging(true) } } useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (isDragging) { const newX = e.clientX - dragOffset.x const newY = e.clientY - dragOffset.y const size = { width: panelRef.current?.offsetWidth || PANEL_DIMENSIONS.width, height: panelRef.current?.offsetHeight || PANEL_DIMENSIONS.height, } const newPosition = clampToViewport({ x: newX, y: newY }, size) setPanelCentered(false) setPosition(newPosition) // 保存位置到localStorage localStorage.setItem('rankingPanelPosition', JSON.stringify(newPosition)) } } const handleMouseUp = () => { setIsDragging(false) } if (isDragging) { document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) } return () => { document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) } }, [isDragging, dragOffset, position]) // 从localStorage恢复位置 useEffect(() => { const savedPosition = localStorage.getItem('rankingPanelPosition') if (savedPosition) { try { const pos = JSON.parse(savedPosition) setPosition(clampPanelPosition(pos)) setPanelCentered(false) } catch (e) { // 忽略解析错误 } } // 恢复缩略按钮位置 const savedButtonPosition = localStorage.getItem('rankingPanelButtonPosition') if (savedButtonPosition) { try { const pos = JSON.parse(savedButtonPosition) setButtonPosition(clampButtonPosition({ x: pos.x, y: pos.y })) // 恢复居中状态 setButtonCentered(pos.centered === true) } catch (e) { // 忽略解析错误 } } }, []) useEffect(() => { const handleResize = () => { setPosition((prev) => clampPanelPosition(prev)) setButtonPosition((prev) => clampButtonPosition(prev)) } window.addEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize) }, []) // 缩略按钮拖动处理 const handleButtonMouseDown = (e: React.MouseEvent) => { if (buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect() setButtonDragOffset({ x: e.clientX - rect.left, y: e.clientY - rect.top }) setIsButtonDragging(true) } } useEffect(() => { const handleButtonMouseMove = (e: MouseEvent) => { if (isButtonDragging && buttonRef.current) { const newX = e.clientX - buttonDragOffset.x const newY = e.clientY - buttonDragOffset.y const size = { width: buttonRef.current.offsetWidth || BUTTON_SIZE, height: buttonRef.current.offsetHeight || BUTTON_SIZE, } const newPosition = clampToViewport({ x: newX, y: newY }, size) setButtonCentered(false) setButtonPosition(newPosition) // 保存位置到localStorage localStorage.setItem('rankingPanelButtonPosition', JSON.stringify(newPosition)) } } const handleButtonMouseUp = () => { setIsButtonDragging(false) } if (isButtonDragging) { document.addEventListener('mousemove', handleButtonMouseMove) document.addEventListener('mouseup', handleButtonMouseUp) } return () => { document.removeEventListener('mousemove', handleButtonMouseMove) document.removeEventListener('mouseup', handleButtonMouseUp) } }, [isButtonDragging, buttonDragOffset]) const currentTabMeta = useMemo(() => TAB_META[currentTab], [currentTab]) // 必须在组件顶层调用 hooks const clickStartRef = useRef<{ x: number; y: number; time: number } | null>(null) if (!isVisible) { // 显示一个小的重新打开按钮(可拖动) return ( ) } const getMedalIcon = (index: number) => { switch (index) { case 0: return ( ) case 1: return ( ) case 2: return ( ) default: return null } } const getCurrentData = () => { switch (currentTab) { case 'contributors': return rankings.contributors case 'downloads': return rankings.downloads case 'likes': return rankings.likes } } return (
{/* 拖动区域和关闭按钮 */}
{/* 关闭按钮 */} {/* 标题 */}

{currentTabMeta.label}

{/* 指示器 */}
{(Object.keys(TAB_META) as PanelTab[]).map((tabKey) => { const isActive = tabKey === currentTab return ( ) })}
{/* 排行榜内容 */}
{loading ? (
加载中...
) : getCurrentData().length === 0 ? (
暂无数据
) : (
{getCurrentData().map((user, index) => (
{/* 排名/奖牌 */}
{index < 3 ? ( getMedalIcon(index) ) : ( {index + 1} )}
{/* 用户名 - 可点击查看作者作品 */}
{user.username}
{/* 数量 */}
{user.count}
))}
)}
) }