添加每日一练排行榜功能

- 修复 daily_exam_service.go 中的类型转换错误
- 在首页添加每日一练排行榜组件
- 显示今日每日一练的考试成绩和用时排行
- 当今日尚未生成每日一练时显示友好提示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
燕陇琪 2025-12-02 00:31:33 +08:00
parent 960f557ca4
commit b1551e6deb
2 changed files with 120 additions and 2 deletions

View File

@ -75,7 +75,7 @@ func (s *DailyExamService) GenerateDailyExam() error {
} }
exam := models.Exam{ exam := models.Exam{
UserID: firstUser.ID, // 使用第一个用户作为创建者 UserID: uint(firstUser.ID), // 使用第一个用户作为创建者
Title: title, Title: title,
TotalScore: int(totalScore), TotalScore: int(totalScore),
Duration: 60, Duration: 60,
@ -92,7 +92,7 @@ func (s *DailyExamService) GenerateDailyExam() error {
log.Printf("成功创建每日一练试卷: ID=%d, Title=%s", exam.ID, exam.Title) log.Printf("成功创建每日一练试卷: ID=%d, Title=%s", exam.ID, exam.Title)
// 6. 分享给所有用户 // 6. 分享给所有用户
if err := s.shareToAllUsers(exam.ID, firstUser.ID); err != nil { if err := s.shareToAllUsers(exam.ID, uint(firstUser.ID)); err != nil {
log.Printf("分享试卷失败: %v", err) log.Printf("分享试卷失败: %v", err)
// 不返回错误,因为试卷已创建成功 // 不返回错误,因为试卷已创建成功
} }

View File

@ -24,6 +24,7 @@ import {
CrownOutlined, CrownOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import * as questionApi from '../api/question' import * as questionApi from '../api/question'
import * as examApi from '../api/exam'
import { fetchWithAuth } from '../utils/request' import { fetchWithAuth } from '../utils/request'
import type { Statistics } from '../types/question' import type { Statistics } from '../types/question'
import styles from './Home.module.less' import styles from './Home.module.less'
@ -110,6 +111,23 @@ const Home: React.FC = () => {
const [rankingType, setRankingType] = useState<'daily' | 'total'>('daily') // 排行榜类型:每日或总榜 const [rankingType, setRankingType] = useState<'daily' | 'total'>('daily') // 排行榜类型:每日或总榜
const [sliderPosition, setSliderPosition] = useState<'left' | 'right'>('left') // 滑块位置 const [sliderPosition, setSliderPosition] = useState<'left' | 'right'>('left') // 滑块位置
// 每日一练排行榜状态
const [dailyExamRanking, setDailyExamRanking] = useState<{
exam_id?: number
exam_title?: string
rankings: Array<{
user_id: number
username: string
nickname: string
avatar: string
score: number
time_spent: number
rank: number
}>
total: number
}>({ rankings: [], total: 0 })
const [dailyExamLoading, setDailyExamLoading] = useState(false)
// 答题设置状态 // 答题设置状态
const [autoNext, setAutoNext] = useState(() => { const [autoNext, setAutoNext] = useState(() => {
const saved = localStorage.getItem('autoNextEnabled') const saved = localStorage.getItem('autoNextEnabled')
@ -183,6 +201,23 @@ const Home: React.FC = () => {
setSliderPosition(type === 'daily' ? 'left' : 'right') setSliderPosition(type === 'daily' ? 'left' : 'right')
} }
// 加载每日一练排行榜
const loadDailyExamRanking = async () => {
setDailyExamLoading(true)
try {
const res = await examApi.getDailyExamRanking()
if (res.success && res.data) {
setDailyExamRanking(res.data)
}
} catch (error) {
console.error('加载每日一练排行榜失败:', error)
// 如果失败,设置为空数据(可能是今日尚未生成)
setDailyExamRanking({ rankings: [], total: 0 })
} finally {
setDailyExamLoading(false)
}
}
// 加载用户信息 // 加载用户信息
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
@ -206,6 +241,7 @@ const Home: React.FC = () => {
useEffect(() => { useEffect(() => {
loadStatistics() loadStatistics()
loadCurrentRanking() loadCurrentRanking()
loadDailyExamRanking()
}, [rankingType]) }, [rankingType])
// 动态加载聊天插件(仅在首页加载) // 动态加载聊天插件(仅在首页加载)
@ -823,6 +859,88 @@ const Home: React.FC = () => {
)} )}
</div> </div>
{/* 每日一练排行榜 */}
<div className={styles.rankingSection}>
<Title level={4} className={styles.sectionTitle}>
<CrownOutlined style={{ color: '#fa8c16' }} />
</Title>
{dailyExamLoading ? (
<Card className={styles.rankingCard} loading={true} />
) : dailyExamRanking.rankings.length === 0 ? (
<Card className={styles.rankingCard}>
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#8c8c8c' }}>
<CrownOutlined style={{ fontSize: 48, marginBottom: 16, opacity: 0.3, color: '#fa8c16' }} />
<div></div>
<div style={{ fontSize: 13, marginTop: 8 }}>1</div>
</div>
</Card>
) : (
<Card className={styles.rankingCard}>
{dailyExamRanking.exam_title && (
<div style={{
padding: '12px 16px',
background: 'linear-gradient(135deg, #fff7e6 0%, #ffe7ba 100%)',
borderRadius: '8px',
marginBottom: '16px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<FileTextOutlined style={{ color: '#fa8c16', fontSize: 16 }} />
<Text strong style={{ color: '#fa8c16' }}>{dailyExamRanking.exam_title}</Text>
</div>
)}
<div className={styles.rankingList}>
{dailyExamRanking.rankings.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} style={{
color: user.score >= 80 ? '#52c41a' : user.score >= 60 ? '#faad14' : '#ff4d4f',
fontSize: 18,
fontWeight: 'bold'
}}>
{user.score}
</div>
<div className={styles.rankStatLabel}></div>
</div>
<div className={styles.rankDivider}></div>
<div className={styles.rankStat}>
<div className={styles.rankStatValue}>
{Math.floor(user.time_spent / 60)}'
{user.time_spent % 60 < 10 ? '0' : ''}{user.time_spent % 60}"
</div>
<div className={styles.rankStatLabel}></div>
</div>
</div>
</div>
))}
</div>
</Card>
)}
</div>
{/* 用户类型补充模态框 */} {/* 用户类型补充模态框 */}
<Modal <Modal
title="请选择您的身份类型" title="请选择您的身份类型"