实现总排行榜功能 1. 在后端添加GetTotalRanking函数和API路由 2. 在前端添加总排行榜展示和切换功能 3. 用户现在可以在首页切换查看今日排行榜和总排行榜
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
69ae78b009
commit
6efc437198
@ -84,6 +84,61 @@ func GetDailyRanking(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetTotalRanking 获取总排行榜
|
||||
func GetTotalRanking(c *gin.Context) {
|
||||
db := database.GetDB()
|
||||
|
||||
// 获取查询参数
|
||||
limitStr := c.DefaultQuery("limit", "10")
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit <= 0 || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
// 查询总排行榜(按总答题数量和正确率排序)
|
||||
var rankings []models.UserStats
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
u.id as user_id,
|
||||
u.username,
|
||||
u.nickname,
|
||||
u.avatar,
|
||||
u.user_type,
|
||||
COALESCE(COUNT(ar.id), 0) as total_answers,
|
||||
COALESCE(SUM(CASE WHEN ar.is_correct = true THEN 1 ELSE 0 END), 0) as correct_count,
|
||||
COALESCE(SUM(CASE WHEN ar.is_correct = false THEN 1 ELSE 0 END), 0) as wrong_count,
|
||||
CASE
|
||||
WHEN COUNT(ar.id) > 0 THEN
|
||||
ROUND(CAST(CAST(SUM(CASE WHEN ar.is_correct = true THEN 1 ELSE 0 END) AS FLOAT) / COUNT(ar.id) * 100 AS NUMERIC), 2)
|
||||
ELSE 0
|
||||
END as accuracy,
|
||||
u.created_at,
|
||||
MAX(ar.answered_at) as last_answer_at
|
||||
FROM users u
|
||||
LEFT JOIN user_answer_records ar ON u.id = ar.user_id AND ar.deleted_at IS NULL
|
||||
WHERE u.deleted_at IS NULL
|
||||
GROUP BY u.id, u.username, u.nickname, u.avatar, u.user_type, u.created_at
|
||||
HAVING COUNT(ar.id) > 0
|
||||
ORDER BY total_answers DESC, accuracy DESC, correct_count DESC
|
||||
LIMIT ?
|
||||
`
|
||||
|
||||
if err := db.Raw(query, limit).Scan(&rankings).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": "获取总排行榜数据失败",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": rankings,
|
||||
})
|
||||
}
|
||||
|
||||
// StaticFileHandler 静态文件处理器,用于服务前端静态资源
|
||||
// 使用 NoRoute 避免与 API 路由冲突
|
||||
func StaticFileHandler(root string) http.Handler {
|
||||
|
||||
1
main.go
1
main.go
@ -45,6 +45,7 @@ func main() {
|
||||
|
||||
// 排行榜API
|
||||
auth.GET("/ranking/daily", handlers.GetDailyRanking) // 获取今日排行榜
|
||||
auth.GET("/ranking/total", handlers.GetTotalRanking) // 获取总排行榜
|
||||
|
||||
// 练习题相关API(需要登录)
|
||||
auth.GET("/practice/questions", handlers.GetPracticeQuestions) // 获取练习题目列表
|
||||
|
||||
@ -199,3 +199,8 @@ export const getUserDetailStats = (userId: number) => {
|
||||
export const getDailyRanking = (limit: number = 10) => {
|
||||
return request.get<ApiResponse<UserStats[]>>('/ranking/daily', { params: { limit } })
|
||||
}
|
||||
|
||||
// 获取总排行榜
|
||||
export const getTotalRanking = (limit: number = 10) => {
|
||||
return request.get<ApiResponse<UserStats[]>>('/ranking/total', { params: { limit } })
|
||||
}
|
||||
|
||||
@ -105,7 +105,9 @@ const Home: React.FC = () => {
|
||||
|
||||
// 排行榜状态
|
||||
const [dailyRanking, setDailyRanking] = useState<questionApi.UserStats[]>([])
|
||||
const [totalRanking, setTotalRanking] = useState<questionApi.UserStats[]>([])
|
||||
const [rankingLoading, setRankingLoading] = useState(false)
|
||||
const [rankingType, setRankingType] = useState<'daily' | 'total'>('daily') // 排行榜类型:每日或总榜
|
||||
|
||||
// 答题设置状态
|
||||
const [autoNext, setAutoNext] = useState(() => {
|
||||
@ -150,6 +152,30 @@ const Home: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载总排行榜数据
|
||||
const loadTotalRanking = async () => {
|
||||
setRankingLoading(true)
|
||||
try {
|
||||
const res = await questionApi.getTotalRanking(10)
|
||||
if (res.success && res.data) {
|
||||
setTotalRanking(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载总排行榜失败:', error)
|
||||
} finally {
|
||||
setRankingLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载当前选中的排行榜数据
|
||||
const loadCurrentRanking = async () => {
|
||||
if (rankingType === 'daily') {
|
||||
await loadDailyRanking()
|
||||
} else {
|
||||
await loadTotalRanking()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载用户信息
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
@ -172,8 +198,8 @@ const Home: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
loadStatistics()
|
||||
loadDailyRanking()
|
||||
}, [])
|
||||
loadCurrentRanking()
|
||||
}, [rankingType])
|
||||
|
||||
// 动态加载聊天插件(仅在首页加载)
|
||||
useEffect(() => {
|
||||
@ -655,25 +681,93 @@ const Home: React.FC = () => {
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* 今日排行榜 */}
|
||||
{/* 排行榜 */}
|
||||
<div className={styles.rankingSection}>
|
||||
<Title level={4} className={styles.sectionTitle}>
|
||||
<TrophyOutlined /> 今日排行榜
|
||||
<TrophyOutlined /> 排行榜
|
||||
</Title>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button
|
||||
type={rankingType === 'daily' ? 'primary' : 'default'}
|
||||
onClick={() => setRankingType('daily')}
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
今日排行榜
|
||||
</Button>
|
||||
<Button
|
||||
type={rankingType === 'total' ? 'primary' : 'default'}
|
||||
onClick={() => setRankingType('total')}
|
||||
>
|
||||
总排行榜
|
||||
</Button>
|
||||
</div>
|
||||
{rankingLoading ? (
|
||||
<Card className={styles.rankingCard} loading={true} />
|
||||
) : dailyRanking.length === 0 ? (
|
||||
) : rankingType === 'daily' ? (
|
||||
dailyRanking.length === 0 ? (
|
||||
<Card className={styles.rankingCard}>
|
||||
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#8c8c8c' }}>
|
||||
<TrophyOutlined style={{ fontSize: 48, marginBottom: 16, opacity: 0.3 }} />
|
||||
<div>今日暂无排行数据</div>
|
||||
<div style={{ fontSize: 13, marginTop: 8 }}>快去刷题吧!</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className={styles.rankingCard}>
|
||||
<div className={styles.rankingList}>
|
||||
{dailyRanking.map((user, index) => (
|
||||
<div key={user.user_id} className={styles.rankingItem}>
|
||||
<div className={styles.rankingLeft}>
|
||||
{index < 3 ? (
|
||||
<div className={`${styles.rankBadge} ${styles[`rank${index + 1}`]}`}>
|
||||
{index === 0 && <CrownOutlined />}
|
||||
{index === 1 && <CrownOutlined />}
|
||||
{index === 2 && <CrownOutlined />}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.rankNumber}>{index + 1}</div>
|
||||
)}
|
||||
<Avatar
|
||||
src={user.avatar || undefined}
|
||||
size={40}
|
||||
icon={<UserOutlined />}
|
||||
className={styles.rankAvatar}
|
||||
/>
|
||||
<div className={styles.rankUserInfo}>
|
||||
<div className={styles.rankNickname}>{user.nickname}</div>
|
||||
<div className={styles.rankUsername}>@{user.username}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rankingRight}>
|
||||
<div className={styles.rankStat}>
|
||||
<div className={styles.rankStatValue}>{user.total_answers}</div>
|
||||
<div className={styles.rankStatLabel}>今日刷题</div>
|
||||
</div>
|
||||
<div className={styles.rankDivider}></div>
|
||||
<div className={styles.rankStat}>
|
||||
<div className={styles.rankStatValue} style={{ color: user.accuracy >= 80 ? '#52c41a' : user.accuracy >= 60 ? '#faad14' : '#ff4d4f' }}>
|
||||
{user.accuracy.toFixed(0)}%
|
||||
</div>
|
||||
<div className={styles.rankStatLabel}>正确率</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
) : totalRanking.length === 0 ? (
|
||||
<Card className={styles.rankingCard}>
|
||||
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#8c8c8c' }}>
|
||||
<TrophyOutlined style={{ fontSize: 48, marginBottom: 16, opacity: 0.3 }} />
|
||||
<div>今日暂无排行数据</div>
|
||||
<div>暂无排行数据</div>
|
||||
<div style={{ fontSize: 13, marginTop: 8 }}>快去刷题吧!</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className={styles.rankingCard}>
|
||||
<div className={styles.rankingList}>
|
||||
{dailyRanking.map((user, index) => (
|
||||
{totalRanking.map((user, index) => (
|
||||
<div key={user.user_id} className={styles.rankingItem}>
|
||||
<div className={styles.rankingLeft}>
|
||||
{index < 3 ? (
|
||||
@ -699,7 +793,7 @@ const Home: React.FC = () => {
|
||||
<div className={styles.rankingRight}>
|
||||
<div className={styles.rankStat}>
|
||||
<div className={styles.rankStatValue}>{user.total_answers}</div>
|
||||
<div className={styles.rankStatLabel}>今日刷题</div>
|
||||
<div className={styles.rankStatLabel}>总刷题</div>
|
||||
</div>
|
||||
<div className={styles.rankDivider}></div>
|
||||
<div className={styles.rankStat}>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user