添加每日一练排行榜功能
- 修复 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:
parent
960f557ca4
commit
b1551e6deb
@ -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)
|
||||||
// 不返回错误,因为试卷已创建成功
|
// 不返回错误,因为试卷已创建成功
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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="请选择您的身份类型"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user