🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1070 lines
37 KiB
TypeScript
1070 lines
37 KiB
TypeScript
import React, { useEffect, useState } from 'react'
|
||
import { useNavigate } from 'react-router-dom'
|
||
import { Typography, message, Space, Avatar, Button, Modal, Form, Radio, Alert, Input, Switch, InputNumber, Dropdown, Row, Col, Card } from 'antd'
|
||
import type { MenuProps } from 'antd'
|
||
import {
|
||
FileTextOutlined,
|
||
CheckCircleOutlined,
|
||
UnorderedListOutlined,
|
||
EditOutlined,
|
||
RocketOutlined,
|
||
BookOutlined,
|
||
UserOutlined,
|
||
LogoutOutlined,
|
||
SettingOutlined,
|
||
UnorderedListOutlined as ListOutlined,
|
||
LockOutlined,
|
||
IdcardOutlined,
|
||
MoreOutlined,
|
||
CloseCircleOutlined,
|
||
FormOutlined,
|
||
FileMarkdownOutlined,
|
||
TeamOutlined,
|
||
TrophyOutlined,
|
||
CrownOutlined,
|
||
} from '@ant-design/icons'
|
||
import * as questionApi from '../api/question'
|
||
import { fetchWithAuth } from '../utils/request'
|
||
import type { Statistics } from '../types/question'
|
||
import styles from './Home.module.less'
|
||
|
||
const { Title, Paragraph, Text } = Typography
|
||
|
||
// 题型配置 - 使用数据库中的实际类型,采用明快的配色方案
|
||
const questionTypes = [
|
||
{
|
||
key: 'multiple-choice',
|
||
title: '选择题',
|
||
icon: <CheckCircleOutlined />,
|
||
color: '#1890ff', // 明亮的蓝色
|
||
description: '基础知识考察',
|
||
},
|
||
{
|
||
key: 'multiple-selection',
|
||
title: '多选题',
|
||
icon: <UnorderedListOutlined />,
|
||
color: '#52c41a', // 明亮的绿色
|
||
description: '综合能力提升',
|
||
},
|
||
{
|
||
key: 'true-false',
|
||
title: '判断题',
|
||
icon: <CloseCircleOutlined />,
|
||
color: '#fa8c16', // 明亮的橙色
|
||
description: '快速判断训练',
|
||
},
|
||
{
|
||
key: 'fill-in-blank',
|
||
title: '填空题',
|
||
icon: <FormOutlined />,
|
||
color: '#faad14', // 明亮的金色
|
||
description: '填空补充练习',
|
||
},
|
||
{
|
||
key: 'short-answer',
|
||
title: '简答题',
|
||
icon: <EditOutlined />,
|
||
color: '#722ed1', // 明亮的紫色
|
||
description: '深度理解练习',
|
||
},
|
||
{
|
||
key: 'essay', // 特殊标识,根据用户类型动态路由
|
||
title: '论述题',
|
||
icon: <FileMarkdownOutlined />,
|
||
color: '#eb2f96', // 明亮的粉色
|
||
description: '深度分析与表达',
|
||
},
|
||
]
|
||
|
||
interface UserInfo {
|
||
username: string
|
||
nickname: string
|
||
avatar: string
|
||
user_type?: string // 用户类型
|
||
}
|
||
|
||
const Home: React.FC = () => {
|
||
const navigate = useNavigate()
|
||
const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
|
||
const [userTypeModalVisible, setUserTypeModalVisible] = useState(false) // 用户类型补充模态框
|
||
const [editProfileVisible, setEditProfileVisible] = useState(false) // 修改用户信息模态框
|
||
const [changePasswordVisible, setChangePasswordVisible] = useState(false) // 修改密码模态框
|
||
const [practiceSettingsVisible, setPracticeSettingsVisible] = useState(false) // 答题设置模态框
|
||
const [loading, setLoading] = useState(false)
|
||
const [userTypeForm] = Form.useForm()
|
||
const [editProfileForm] = Form.useForm()
|
||
const [changePasswordForm] = Form.useForm()
|
||
const [statistics, setStatistics] = useState<Statistics>({
|
||
total_questions: 0,
|
||
answered_questions: 0,
|
||
correct_answers: 0,
|
||
wrong_questions: 0,
|
||
total_answers: 0,
|
||
accuracy: 0,
|
||
})
|
||
|
||
// 排行榜状态
|
||
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 [sliderPosition, setSliderPosition] = useState<'left' | 'right'>('left') // 滑块位置
|
||
|
||
// 答题设置状态
|
||
const [autoNext, setAutoNext] = useState(() => {
|
||
const saved = localStorage.getItem('autoNextEnabled')
|
||
return saved !== null ? saved === 'true' : true
|
||
})
|
||
|
||
const [autoNextDelay, setAutoNextDelay] = useState(() => {
|
||
const saved = localStorage.getItem('autoNextDelay')
|
||
return saved !== null ? parseInt(saved, 10) : 2
|
||
})
|
||
|
||
const [randomMode, setRandomMode] = useState(() => {
|
||
const saved = localStorage.getItem('randomModeEnabled')
|
||
return saved !== null ? saved === 'true' : false
|
||
})
|
||
|
||
// 加载统计数据
|
||
const loadStatistics = async () => {
|
||
try {
|
||
const res = await questionApi.getStatistics()
|
||
if (res.success && res.data) {
|
||
setStatistics(res.data)
|
||
}
|
||
} catch (error) {
|
||
console.error('加载统计失败:', error)
|
||
}
|
||
}
|
||
|
||
// 加载排行榜数据
|
||
const loadDailyRanking = async () => {
|
||
setRankingLoading(true)
|
||
try {
|
||
const res = await questionApi.getDailyRanking(10)
|
||
if (res.success && res.data) {
|
||
setDailyRanking(res.data)
|
||
}
|
||
} catch (error) {
|
||
console.error('加载排行榜失败:', error)
|
||
} finally {
|
||
setRankingLoading(false)
|
||
}
|
||
}
|
||
|
||
// 加载总排行榜数据
|
||
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()
|
||
}
|
||
}
|
||
|
||
// 切换排行榜类型
|
||
const switchRankingType = (type: 'daily' | 'total') => {
|
||
setRankingType(type)
|
||
setSliderPosition(type === 'daily' ? 'left' : 'right')
|
||
}
|
||
|
||
// 加载用户信息
|
||
useEffect(() => {
|
||
const token = localStorage.getItem('token')
|
||
const savedUserInfo = localStorage.getItem('user')
|
||
|
||
if (token && savedUserInfo) {
|
||
try {
|
||
const user = JSON.parse(savedUserInfo)
|
||
setUserInfo(user)
|
||
|
||
// 检查用户是否有用户类型,如果没有则显示强制选择模态框
|
||
if (!user.user_type) {
|
||
setUserTypeModalVisible(true)
|
||
}
|
||
} catch (e) {
|
||
console.error('解析用户信息失败', e)
|
||
}
|
||
}
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
loadStatistics()
|
||
loadCurrentRanking()
|
||
}, [rankingType])
|
||
|
||
// 动态加载聊天插件(仅在首页加载)
|
||
useEffect(() => {
|
||
const script = document.createElement('script')
|
||
script.src = 'http://xiwang.nianliuxi.com:8282/chat/api/embed?protocol=http&host=xiwang.nianliuxi.com:8282&token=f131cc7227f4ee8e'
|
||
script.async = true
|
||
script.defer = true
|
||
script.id = 'chat-embed-script' // 添加 ID 以便于查找和清理
|
||
document.body.appendChild(script)
|
||
|
||
// 组件卸载时清理脚本和聊天插件元素
|
||
return () => {
|
||
// 移除脚本标签
|
||
const scriptElement = document.getElementById('chat-embed-script')
|
||
if (scriptElement) {
|
||
document.body.removeChild(scriptElement)
|
||
}
|
||
|
||
// 清理聊天插件可能创建的 DOM 元素
|
||
// 根据聊天插件的实际 DOM 结构调整选择器
|
||
const chatElements = document.querySelectorAll('[id*="chat"], [class*="chat"], [id*="embed"], [class*="embed"]')
|
||
chatElements.forEach(element => {
|
||
if (element.parentNode) {
|
||
element.parentNode.removeChild(element)
|
||
}
|
||
})
|
||
}
|
||
}, [])
|
||
|
||
// 处理用户类型更新
|
||
const handleUpdateUserType = async (values: { user_type: string }) => {
|
||
setLoading(true)
|
||
try {
|
||
const response = await fetchWithAuth('/api/user/type', {
|
||
method: 'PUT',
|
||
body: JSON.stringify(values),
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
// 更新本地存储的用户信息
|
||
const user = JSON.parse(localStorage.getItem('user') || '{}')
|
||
user.user_type = data.data.user_type
|
||
localStorage.setItem('user', JSON.stringify(user))
|
||
setUserInfo(user)
|
||
|
||
message.success('身份类型设置成功')
|
||
setUserTypeModalVisible(false)
|
||
} else {
|
||
message.error(data.message || '更新失败')
|
||
}
|
||
} catch (err) {
|
||
message.error('网络错误,请稍后重试')
|
||
console.error('更新用户类型错误:', err)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 修改用户信息
|
||
const handleEditProfile = () => {
|
||
if (userInfo) {
|
||
editProfileForm.setFieldsValue({
|
||
nickname: userInfo.nickname,
|
||
user_type: userInfo.user_type,
|
||
})
|
||
setEditProfileVisible(true)
|
||
}
|
||
}
|
||
|
||
const handleUpdateProfile = async (values: { nickname: string; user_type: string }) => {
|
||
setLoading(true)
|
||
try {
|
||
const response = await fetchWithAuth('/api/user/profile', {
|
||
method: 'PUT',
|
||
body: JSON.stringify(values),
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
// 更新本地存储的用户信息
|
||
const user = JSON.parse(localStorage.getItem('user') || '{}')
|
||
user.nickname = data.data.nickname
|
||
user.user_type = data.data.user_type
|
||
localStorage.setItem('user', JSON.stringify(user))
|
||
setUserInfo(user)
|
||
|
||
message.success('个人信息更新成功')
|
||
setEditProfileVisible(false)
|
||
} else {
|
||
message.error(data.message || '更新失败')
|
||
}
|
||
} catch (err) {
|
||
message.error('网络错误,请稍后重试')
|
||
console.error('更新用户信息错误:', err)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 修改密码
|
||
const handleChangePassword = async (values: { old_password: string; new_password: string }) => {
|
||
setLoading(true)
|
||
try {
|
||
const response = await fetchWithAuth('/api/user/password', {
|
||
method: 'PUT',
|
||
body: JSON.stringify(values),
|
||
})
|
||
|
||
const data = await response.json()
|
||
|
||
if (data.success) {
|
||
message.success('密码修改成功,请重新登录')
|
||
changePasswordForm.resetFields()
|
||
setChangePasswordVisible(false)
|
||
// 清除登录信息并跳转到登录页
|
||
setTimeout(() => {
|
||
localStorage.removeItem('token')
|
||
localStorage.removeItem('user')
|
||
navigate('/login')
|
||
}, 1000)
|
||
} else {
|
||
message.error(data.message || '修改失败')
|
||
}
|
||
} catch (err) {
|
||
message.error('网络错误,请稍后重试')
|
||
console.error('修改密码错误:', err)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 答题设置相关函数
|
||
const toggleAutoNext = () => {
|
||
const newValue = !autoNext
|
||
setAutoNext(newValue)
|
||
localStorage.setItem('autoNextEnabled', String(newValue))
|
||
}
|
||
|
||
const toggleRandomMode = () => {
|
||
const newValue = !randomMode
|
||
setRandomMode(newValue)
|
||
localStorage.setItem('randomModeEnabled', String(newValue))
|
||
}
|
||
|
||
const handleDelayChange = (value: number | null) => {
|
||
if (value !== null && value >= 1 && value <= 10) {
|
||
setAutoNextDelay(value)
|
||
localStorage.setItem('autoNextDelay', String(value))
|
||
}
|
||
}
|
||
|
||
// 获取用户类型显示文本
|
||
const getUserTypeText = (type?: string) => {
|
||
if (!type) return '未设置'
|
||
return type === 'ordinary-person' ? '普通涉密人员' : '保密管理人员'
|
||
}
|
||
|
||
// 退出登录
|
||
const handleLogout = () => {
|
||
Modal.confirm({
|
||
title: '确定要退出登录吗?',
|
||
onOk: () => {
|
||
localStorage.removeItem('token')
|
||
localStorage.removeItem('user')
|
||
setUserInfo(null)
|
||
message.success('已退出登录')
|
||
// 跳转到登录页并强制刷新,确保清理聊天插件
|
||
window.location.href = '/login'
|
||
},
|
||
})
|
||
}
|
||
|
||
// 用户菜单项
|
||
const userMenuItems: MenuProps['items'] = [
|
||
{
|
||
key: 'edit-profile',
|
||
icon: <IdcardOutlined />,
|
||
label: '修改个人信息',
|
||
onClick: handleEditProfile,
|
||
},
|
||
{
|
||
key: 'change-password',
|
||
icon: <LockOutlined />,
|
||
label: '修改密码',
|
||
onClick: () => setChangePasswordVisible(true),
|
||
},
|
||
{
|
||
key: 'practice-settings',
|
||
icon: <SettingOutlined />,
|
||
label: '答题设置',
|
||
onClick: () => setPracticeSettingsVisible(true),
|
||
},
|
||
{
|
||
type: 'divider',
|
||
},
|
||
{
|
||
key: 'logout',
|
||
icon: <LogoutOutlined />,
|
||
label: '退出登录',
|
||
danger: true,
|
||
onClick: handleLogout,
|
||
},
|
||
]
|
||
|
||
// 点击题型卡片
|
||
const handleTypeClick = async (type: string) => {
|
||
try {
|
||
// 如果是论述题,根据用户类型动态确定题型
|
||
let actualType = type
|
||
if (type === 'essay') {
|
||
if (!userInfo?.user_type) {
|
||
message.warning('请先设置您的身份类型')
|
||
return
|
||
}
|
||
// 根据用户类型选择对应的论述题
|
||
actualType = userInfo.user_type === 'ordinary-person' ? 'ordinary-essay' : 'management-essay'
|
||
}
|
||
|
||
// 加载该题型的题目列表
|
||
const res = await questionApi.getQuestions({ type: actualType })
|
||
if (res.success && res.data && res.data.length > 0) {
|
||
// 跳转到答题页面,并传递题型参数
|
||
navigate(`/question?type=${actualType}`)
|
||
} else {
|
||
message.warning('该题型暂无题目')
|
||
}
|
||
} catch (error) {
|
||
message.error('加载题目失败')
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className={styles.container}>
|
||
{/* 头部 */}
|
||
<div className={styles.header}>
|
||
<div className={styles.headerLeft}>
|
||
<div className={styles.logoArea}>
|
||
<img src="/icon.svg" alt="AnKao Logo" className={styles.logo} />
|
||
<div>
|
||
<div className={styles.titleRow}>
|
||
<Title level={2} className={styles.title}>AnKao 刷题</Title>
|
||
<span className={styles.totalBadge}>{statistics.total_questions} 题</span>
|
||
</div>
|
||
<Paragraph className={styles.subtitle}>安全保密考试题库</Paragraph>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* 用户信息 */}
|
||
{userInfo && (
|
||
<div className={styles.userInfo}>
|
||
<div className={styles.userInfoContent}>
|
||
<div className={styles.avatarWrapper}>
|
||
<Avatar
|
||
src={userInfo.avatar || undefined}
|
||
size={56}
|
||
icon={<UserOutlined />}
|
||
className={styles.userAvatar}
|
||
/>
|
||
<div className={styles.avatarBadge}>
|
||
{userInfo.user_type ? (
|
||
<CheckCircleOutlined />
|
||
) : (
|
||
<SettingOutlined />
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className={styles.userDetails}>
|
||
<Text strong className={styles.userNickname}>{userInfo.nickname}</Text>
|
||
<Text type="secondary" className={styles.userUsername}>@{userInfo.username}</Text>
|
||
<div className={styles.userTypeBadge}>
|
||
<IdcardOutlined className={styles.badgeIcon} />
|
||
<Text className={styles.userTypeText}>
|
||
{getUserTypeText(userInfo.user_type)}
|
||
</Text>
|
||
</div>
|
||
</div>
|
||
<Dropdown menu={{ items: userMenuItems }} trigger={['click']} placement="bottomRight">
|
||
<div className={styles.dropdownTrigger}>
|
||
<MoreOutlined className={styles.dropdownIcon} />
|
||
</div>
|
||
</Dropdown>
|
||
</div>
|
||
<div className={styles.userStatsRow}>
|
||
<div className={styles.statItem}>
|
||
<div className={styles.statValue}>{statistics.total_answers}</div>
|
||
<div className={styles.statLabel}>刷题次数</div>
|
||
</div>
|
||
<div className={styles.statDivider}></div>
|
||
<div className={styles.statItem}>
|
||
<div className={styles.statValue}>{statistics.answered_questions}</div>
|
||
<div className={styles.statLabel}>已刷题目</div>
|
||
</div>
|
||
<div className={styles.statDivider}></div>
|
||
<div className={styles.statItem}>
|
||
<div className={styles.statValue}>{statistics.wrong_questions}</div>
|
||
<div className={styles.statLabel}>错题本数</div>
|
||
</div>
|
||
<div className={styles.statDivider}></div>
|
||
<div className={styles.statItem}>
|
||
<div className={styles.statValue}>{statistics.accuracy.toFixed(0)}%</div>
|
||
<div className={styles.statLabel}>准确率</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 题型选择 */}
|
||
<div className={styles.typeSection}>
|
||
<Title level={4} className={styles.sectionTitle}>
|
||
<FileTextOutlined /> 选择题型
|
||
</Title>
|
||
<Row gutter={[12, 12]}>
|
||
{questionTypes.map(type => (
|
||
<Col xs={12} sm={12} md={8} lg={6} xl={4} key={type.key}>
|
||
<Card
|
||
hoverable
|
||
className={styles.typeCard}
|
||
onClick={() => handleTypeClick(type.key)}
|
||
styles={{
|
||
body: {
|
||
textAlign: 'center',
|
||
padding: '20px 12px',
|
||
}
|
||
}}
|
||
>
|
||
<div
|
||
className={styles.typeIconWrapper}
|
||
style={{
|
||
background: `linear-gradient(135deg, ${type.color}15 0%, ${type.color}08 100%)`,
|
||
borderColor: `${type.color}30`
|
||
}}
|
||
>
|
||
<div className={styles.typeIcon} style={{ color: type.color }}>
|
||
{type.icon}
|
||
</div>
|
||
</div>
|
||
<Title level={5} className={styles.typeTitle}>{type.title}</Title>
|
||
<Paragraph type="secondary" className={styles.typeDesc}>{type.description}</Paragraph>
|
||
</Card>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
</div>
|
||
|
||
{/* 快速开始 */}
|
||
<div className={styles.quickStart}>
|
||
<Title level={4} className={styles.sectionTitle}>
|
||
<RocketOutlined /> 快速开始
|
||
</Title>
|
||
<Row gutter={[12, 12]}>
|
||
<Col xs={24} sm={24} md={12} lg={8}>
|
||
<Card
|
||
hoverable
|
||
className={styles.quickCard}
|
||
onClick={() => navigate('/wrong-questions')}
|
||
>
|
||
<Space align="center" size="middle" style={{ width: '100%' }}>
|
||
<div
|
||
className={styles.quickIconWrapper}
|
||
style={{
|
||
background: 'linear-gradient(135deg, #fff1f0 0%, #ffe7e6 100%)',
|
||
borderColor: '#ffccc7'
|
||
}}
|
||
>
|
||
<BookOutlined className={styles.quickIcon} style={{ color: '#ff4d4f' }} />
|
||
</div>
|
||
<div style={{ flex: 1 }}>
|
||
<Title level={5} style={{ margin: 0 }}>错题本</Title>
|
||
<Paragraph type="secondary" style={{ margin: 0, fontSize: '13px' }}>复习错题,巩固薄弱知识点</Paragraph>
|
||
</div>
|
||
</Space>
|
||
</Card>
|
||
</Col>
|
||
|
||
<Col xs={24} sm={24} md={12} lg={8}>
|
||
<Card
|
||
hoverable
|
||
className={styles.quickCard}
|
||
onClick={() => navigate('/question-list')}
|
||
>
|
||
<Space align="center" size="middle" style={{ width: '100%' }}>
|
||
<div
|
||
className={styles.quickIconWrapper}
|
||
style={{
|
||
background: 'linear-gradient(135deg, #e6f7ff 0%, #d6f0ff 100%)',
|
||
borderColor: '#91caff'
|
||
}}
|
||
>
|
||
<ListOutlined className={styles.quickIcon} style={{ color: '#1890ff' }} />
|
||
</div>
|
||
<div style={{ flex: 1 }}>
|
||
<Title level={5} style={{ margin: 0 }}>题目列表</Title>
|
||
<Paragraph type="secondary" style={{ margin: 0, fontSize: '13px' }}>查看所有题目和答案</Paragraph>
|
||
</div>
|
||
</Space>
|
||
</Card>
|
||
</Col>
|
||
|
||
<Col xs={24} sm={24} md={12} lg={8}>
|
||
<Card
|
||
hoverable
|
||
className={styles.quickCard}
|
||
onClick={() => navigate('/exam/management')}
|
||
>
|
||
<Space align="center" size="middle" style={{ width: '100%' }}>
|
||
<div
|
||
className={styles.quickIconWrapper}
|
||
style={{
|
||
background: 'linear-gradient(135deg, #fff7e6 0%, #ffe7ba 100%)',
|
||
borderColor: '#ffd591'
|
||
}}
|
||
>
|
||
<FileTextOutlined className={styles.quickIcon} style={{ color: '#fa8c16' }} />
|
||
</div>
|
||
<div style={{ flex: 1 }}>
|
||
<Title level={5} style={{ margin: 0 }}>模拟考试</Title>
|
||
<Paragraph type="secondary" style={{ margin: 0, fontSize: '13px' }}>创建试卷,随机组卷在线答题</Paragraph>
|
||
</div>
|
||
</Space>
|
||
</Card>
|
||
</Col>
|
||
|
||
{/* 仅 yanlongqi 用户显示题库管理 */}
|
||
{userInfo?.username === 'yanlongqi' && (
|
||
<>
|
||
<Col xs={24} sm={24} md={12} lg={8}>
|
||
<Card
|
||
hoverable
|
||
className={styles.quickCard}
|
||
onClick={() => navigate('/question-management')}
|
||
>
|
||
<Space align="center" size="middle" style={{ width: '100%' }}>
|
||
<div
|
||
className={styles.quickIconWrapper}
|
||
style={{
|
||
background: 'linear-gradient(135deg, #e6fffb 0%, #d6f5f0 100%)',
|
||
borderColor: '#87e8de'
|
||
}}
|
||
>
|
||
<SettingOutlined className={styles.quickIcon} style={{ color: '#36cfc9' }} />
|
||
</div>
|
||
<div style={{ flex: 1 }}>
|
||
<Title level={5} style={{ margin: 0 }}>题库管理</Title>
|
||
<Paragraph type="secondary" style={{ margin: 0, fontSize: '13px' }}>添加、编辑和删除题目</Paragraph>
|
||
</div>
|
||
</Space>
|
||
</Card>
|
||
</Col>
|
||
|
||
<Col xs={24} sm={24} md={12} lg={8}>
|
||
<Card
|
||
hoverable
|
||
className={styles.quickCard}
|
||
onClick={() => navigate('/user-management')}
|
||
>
|
||
<Space align="center" size="middle" style={{ width: '100%' }}>
|
||
<div
|
||
className={styles.quickIconWrapper}
|
||
style={{
|
||
background: 'linear-gradient(135deg, #f0f5ff 0%, #e6edff 100%)',
|
||
borderColor: '#adc6ff'
|
||
}}
|
||
>
|
||
<TeamOutlined className={styles.quickIcon} style={{ color: '#597ef7' }} />
|
||
</div>
|
||
<div style={{ flex: 1 }}>
|
||
<Title level={5} style={{ margin: 0 }}>用户管理</Title>
|
||
<Paragraph type="secondary" style={{ margin: 0, fontSize: '13px' }}>查看用户答题统计</Paragraph>
|
||
</div>
|
||
</Space>
|
||
</Card>
|
||
</Col>
|
||
</>
|
||
)}
|
||
</Row>
|
||
</div>
|
||
|
||
{/* 排行榜 */}
|
||
<div className={styles.rankingSection}>
|
||
<Title level={4} className={styles.sectionTitle}>
|
||
<TrophyOutlined /> 排行榜
|
||
</Title>
|
||
<div className={styles.rankingSwitch}>
|
||
<div
|
||
className={`${styles.rankingSwitchButton} ${rankingType === 'daily' ? styles.active : ''}`}
|
||
onClick={() => switchRankingType('daily')}
|
||
>
|
||
今日排行榜
|
||
</div>
|
||
<div
|
||
className={`${styles.rankingSwitchButton} ${rankingType === 'total' ? styles.active : ''}`}
|
||
onClick={() => switchRankingType('total')}
|
||
>
|
||
总排行榜
|
||
</div>
|
||
<div
|
||
className={styles.rankingSwitchSlider}
|
||
style={{
|
||
width: 'calc(50% - 4px)',
|
||
left: sliderPosition === 'left' ? '4px' : 'calc(50% + 0px)',
|
||
}}
|
||
/>
|
||
</div>
|
||
{rankingLoading ? (
|
||
<Card className={styles.rankingCard} loading={true} />
|
||
) : 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 style={{ fontSize: 13, marginTop: 8 }}>快去刷题吧!</div>
|
||
</div>
|
||
</Card>
|
||
) : (
|
||
<Card className={styles.rankingCard}>
|
||
<div className={styles.rankingList}>
|
||
{totalRanking.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>
|
||
)}
|
||
</div>
|
||
|
||
{/* 用户类型补充模态框 */}
|
||
<Modal
|
||
title="请选择您的身份类型"
|
||
open={userTypeModalVisible}
|
||
closable={false}
|
||
maskClosable={false}
|
||
keyboard={false}
|
||
footer={null}
|
||
destroyOnClose
|
||
>
|
||
<Alert
|
||
message="论述题需要使用"
|
||
description="为了更好地为您提供相应的论述题内容,请选择您的身份类型。"
|
||
type="info"
|
||
showIcon
|
||
style={{ marginBottom: 24 }}
|
||
/>
|
||
<Form
|
||
form={userTypeForm}
|
||
name="userType"
|
||
onFinish={handleUpdateUserType}
|
||
autoComplete="off"
|
||
size="large"
|
||
>
|
||
<Form.Item
|
||
name="user_type"
|
||
label="身份类型"
|
||
rules={[{ required: true, message: '请选择身份类型' }]}
|
||
>
|
||
<Radio.Group>
|
||
<Radio value="ordinary-person">普通涉密人员</Radio>
|
||
<Radio value="management-person">保密管理人员</Radio>
|
||
</Radio.Group>
|
||
</Form.Item>
|
||
|
||
<Form.Item>
|
||
<Button type="primary" htmlType="submit" block loading={loading}>
|
||
确认
|
||
</Button>
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
{/* 修改个人信息模态框 */}
|
||
<Modal
|
||
title="修改个人信息"
|
||
open={editProfileVisible}
|
||
onCancel={() => setEditProfileVisible(false)}
|
||
footer={null}
|
||
destroyOnClose
|
||
>
|
||
<Form
|
||
form={editProfileForm}
|
||
name="editProfile"
|
||
onFinish={handleUpdateProfile}
|
||
autoComplete="off"
|
||
layout="vertical"
|
||
>
|
||
<Form.Item
|
||
name="nickname"
|
||
label="姓名"
|
||
rules={[{ required: true, message: '请输入姓名' }]}
|
||
>
|
||
<Input prefix={<UserOutlined />} placeholder="请输入姓名" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="user_type"
|
||
label="身份类型"
|
||
rules={[{ required: true, message: '请选择身份类型' }]}
|
||
>
|
||
<Radio.Group>
|
||
<Radio value="ordinary-person">普通涉密人员</Radio>
|
||
<Radio value="management-person">保密管理人员</Radio>
|
||
</Radio.Group>
|
||
</Form.Item>
|
||
|
||
<Form.Item>
|
||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||
<Button onClick={() => setEditProfileVisible(false)}>
|
||
取消
|
||
</Button>
|
||
<Button type="primary" htmlType="submit" loading={loading}>
|
||
保存
|
||
</Button>
|
||
</Space>
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
{/* 修改密码模态框 */}
|
||
<Modal
|
||
title="修改密码"
|
||
open={changePasswordVisible}
|
||
onCancel={() => setChangePasswordVisible(false)}
|
||
footer={null}
|
||
destroyOnClose
|
||
>
|
||
<Alert
|
||
message="密码修改后需要重新登录"
|
||
type="warning"
|
||
showIcon
|
||
style={{ marginBottom: 16 }}
|
||
/>
|
||
<Form
|
||
form={changePasswordForm}
|
||
name="changePassword"
|
||
onFinish={handleChangePassword}
|
||
autoComplete="off"
|
||
layout="vertical"
|
||
>
|
||
<Form.Item
|
||
name="old_password"
|
||
label="当前密码"
|
||
rules={[{ required: true, message: '请输入当前密码' }]}
|
||
>
|
||
<Input.Password prefix={<LockOutlined />} placeholder="请输入当前密码" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="new_password"
|
||
label="新密码"
|
||
rules={[
|
||
{ required: true, message: '请输入新密码' },
|
||
{ min: 6, message: '密码长度至少为6位' },
|
||
]}
|
||
>
|
||
<Input.Password prefix={<LockOutlined />} placeholder="请输入新密码(至少6位)" />
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="confirm_password"
|
||
label="确认新密码"
|
||
dependencies={['new_password']}
|
||
rules={[
|
||
{ required: true, message: '请确认新密码' },
|
||
({ getFieldValue }) => ({
|
||
validator(_, value) {
|
||
if (!value || getFieldValue('new_password') === value) {
|
||
return Promise.resolve()
|
||
}
|
||
return Promise.reject(new Error('两次输入的密码不一致'))
|
||
},
|
||
}),
|
||
]}
|
||
>
|
||
<Input.Password prefix={<LockOutlined />} placeholder="请再次输入新密码" />
|
||
</Form.Item>
|
||
|
||
<Form.Item>
|
||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||
<Button onClick={() => setChangePasswordVisible(false)}>
|
||
取消
|
||
</Button>
|
||
<Button type="primary" htmlType="submit" loading={loading}>
|
||
确认修改
|
||
</Button>
|
||
</Space>
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
{/* 答题设置模态框 */}
|
||
<Modal
|
||
title="答题设置"
|
||
open={practiceSettingsVisible}
|
||
onCancel={() => setPracticeSettingsVisible(false)}
|
||
footer={[
|
||
<Button key="close" type="primary" onClick={() => setPracticeSettingsVisible(false)}>
|
||
关闭
|
||
</Button>
|
||
]}
|
||
width={480}
|
||
>
|
||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||
<Alert
|
||
description="这些设置会在答题页面中生效,帮助您更高效地刷题。"
|
||
type="info"
|
||
showIcon
|
||
/>
|
||
|
||
<div>
|
||
<div style={{ marginBottom: 12 }}>
|
||
<span style={{ fontSize: 15, fontWeight: 500 }}>自动下一题</span>
|
||
<span style={{ marginLeft: 8, fontSize: 13, color: '#8c8c8c' }}>
|
||
答对题目后自动跳转到下一题
|
||
</span>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, paddingLeft: 8 }}>
|
||
<Switch
|
||
checked={autoNext}
|
||
onChange={toggleAutoNext}
|
||
/>
|
||
<span style={{ fontSize: 14, color: autoNext ? '#52c41a' : '#8c8c8c' }}>
|
||
{autoNext ? '已开启' : '已关闭'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{autoNext && (
|
||
<div>
|
||
<div style={{ marginBottom: 12 }}>
|
||
<span style={{ fontSize: 15, fontWeight: 500 }}>延迟时间</span>
|
||
<span style={{ marginLeft: 8, fontSize: 13, color: '#8c8c8c' }}>
|
||
设置答对后等待多久跳转到下一题
|
||
</span>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, paddingLeft: 8 }}>
|
||
<InputNumber
|
||
min={1}
|
||
max={10}
|
||
value={autoNextDelay}
|
||
onChange={handleDelayChange}
|
||
style={{ width: 80 }}
|
||
/>
|
||
<span style={{ fontSize: 14 }}>秒后自动跳转</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<div style={{ marginBottom: 12 }}>
|
||
<span style={{ fontSize: 15, fontWeight: 500 }}>随机题目</span>
|
||
<span style={{ marginLeft: 8, fontSize: 13, color: '#8c8c8c' }}>
|
||
开启后点击下一题时随机跳转
|
||
</span>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, paddingLeft: 8 }}>
|
||
<Switch
|
||
checked={randomMode}
|
||
onChange={toggleRandomMode}
|
||
/>
|
||
<span style={{ fontSize: 14, color: randomMode ? '#52c41a' : '#8c8c8c' }}>
|
||
{randomMode ? '已开启' : '已关闭'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</Space>
|
||
</Modal>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default Home
|