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: , color: '#1890ff', // 明亮的蓝色 description: '基础知识考察', }, { key: 'multiple-selection', title: '多选题', icon: , color: '#52c41a', // 明亮的绿色 description: '综合能力提升', }, { key: 'true-false', title: '判断题', icon: , color: '#fa8c16', // 明亮的橙色 description: '快速判断训练', }, { key: 'fill-in-blank', title: '填空题', icon: , color: '#faad14', // 明亮的金色 description: '填空补充练习', }, { key: 'short-answer', title: '简答题', icon: , color: '#722ed1', // 明亮的紫色 description: '深度理解练习', }, { key: 'essay', // 特殊标识,根据用户类型动态路由 title: '论述题', icon: , 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(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({ total_questions: 0, answered_questions: 0, correct_answers: 0, wrong_questions: 0, total_answers: 0, accuracy: 0, }) // 排行榜状态 const [dailyRanking, setDailyRanking] = useState([]) const [totalRanking, setTotalRanking] = useState([]) 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: , label: '修改个人信息', onClick: handleEditProfile, }, { key: 'change-password', icon: , label: '修改密码', onClick: () => setChangePasswordVisible(true), }, { key: 'practice-settings', icon: , label: '答题设置', onClick: () => setPracticeSettingsVisible(true), }, { type: 'divider', }, { key: 'logout', icon: , 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 (
{/* 头部 */}
AnKao Logo
AnKao 刷题 {statistics.total_questions} 题
安全保密考试题库
{/* 用户信息 */} {userInfo && (
} className={styles.userAvatar} />
{userInfo.user_type ? ( ) : ( )}
{userInfo.nickname} @{userInfo.username}
{getUserTypeText(userInfo.user_type)}
{statistics.total_answers}
刷题次数
{statistics.answered_questions}
已刷题目
{statistics.wrong_questions}
错题本数
{statistics.accuracy.toFixed(0)}%
准确率
)}
{/* 题型选择 */}
<FileTextOutlined /> 选择题型 {questionTypes.map(type => ( handleTypeClick(type.key)} styles={{ body: { textAlign: 'center', padding: '20px 12px', } }} >
{type.icon}
{type.title} {type.description}
))}
{/* 快速开始 */}
<RocketOutlined /> 快速开始 navigate('/wrong-questions')} >
错题本 复习错题,巩固薄弱知识点
navigate('/question-list')} >
题目列表 查看所有题目和答案
navigate('/exam/management')} >
模拟考试 创建试卷,随机组卷在线答题
{/* 仅 yanlongqi 用户显示题库管理 */} {userInfo?.username === 'yanlongqi' && ( <> navigate('/question-management')} >
题库管理 添加、编辑和删除题目
navigate('/user-management')} >
用户管理 查看用户答题统计
)}
{/* 排行榜 */}
<TrophyOutlined /> 排行榜
switchRankingType('daily')} > 今日排行榜
switchRankingType('total')} > 总排行榜
{rankingLoading ? ( ) : rankingType === 'daily' ? ( dailyRanking.length === 0 ? (
今日暂无排行数据
快去刷题吧!
) : (
{dailyRanking.map((user, index) => (
{index < 3 ? (
{index === 0 && } {index === 1 && } {index === 2 && }
) : (
{index + 1}
)} } className={styles.rankAvatar} />
{user.nickname}
@{user.username}
{user.total_answers}
今日刷题
= 80 ? '#52c41a' : user.accuracy >= 60 ? '#faad14' : '#ff4d4f' }}> {user.accuracy.toFixed(0)}%
正确率
))}
) ) : totalRanking.length === 0 ? (
暂无排行数据
快去刷题吧!
) : (
{totalRanking.map((user, index) => (
{index < 3 ? (
{index === 0 && } {index === 1 && } {index === 2 && }
) : (
{index + 1}
)} } className={styles.rankAvatar} />
{user.nickname}
@{user.username}
{user.total_answers}
总刷题
= 80 ? '#52c41a' : user.accuracy >= 60 ? '#faad14' : '#ff4d4f' }}> {user.accuracy.toFixed(0)}%
正确率
))}
)}
{/* 用户类型补充模态框 */}
普通涉密人员 保密管理人员
{/* 修改个人信息模态框 */} setEditProfileVisible(false)} footer={null} destroyOnClose >
} placeholder="请输入姓名" /> 普通涉密人员 保密管理人员
{/* 修改密码模态框 */} setChangePasswordVisible(false)} footer={null} destroyOnClose >
} placeholder="请输入当前密码" /> } placeholder="请输入新密码(至少6位)" /> ({ validator(_, value) { if (!value || getFieldValue('new_password') === value) { return Promise.resolve() } return Promise.reject(new Error('两次输入的密码不一致')) }, }), ]} > } placeholder="请再次输入新密码" />
{/* 答题设置模态框 */} setPracticeSettingsVisible(false)} footer={[ ]} width={480} >
自动下一题 答对题目后自动跳转到下一题
{autoNext ? '已开启' : '已关闭'}
{autoNext && (
延迟时间 设置答对后等待多久跳转到下一题
秒后自动跳转
)}
随机题目 开启后点击下一题时随机跳转
{randomMode ? '已开启' : '已关闭'}
) } export default Home