AnCao/web/src/pages/Home.tsx
yanlongqi de8480a328 重构答题系统:组件拆分、进度跟踪、完成统计
主要改动:
1. 组件拆分:将Question.tsx(605行)拆分为4个子组件(303行)
   - QuestionProgress: 进度条和统计显示
   - QuestionCard: 题目卡片和答题界面
   - AnswerResult: 答案结果展示
   - CompletionSummary: 完成统计摘要

2. 新增功能:
   - 答题进度条:显示当前进度、正确数、错误数
   - 进度保存:使用localStorage持久化答题进度
   - 完成统计:答完所有题目后显示统计摘要和正确率
   - 从第一题开始:改为顺序答题而非随机

3. UI优化:
   - 移除右上角统计按钮
   - 移除底部随机题目、题目列表、筛选按钮
   - 移除"开始xxx答题"提示消息
   - 简化页面布局

4. 代码优化:
   - 提高代码可维护性和可测试性
   - 单一职责原则,每个组件负责一个特定功能

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 18:39:15 +08:00

267 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Statistic, Row, Col, Typography, message, Space, Avatar, Button, Modal } from 'antd'
import {
FileTextOutlined,
CheckCircleOutlined,
UnorderedListOutlined,
EditOutlined,
RocketOutlined,
BookOutlined,
UserOutlined,
LogoutOutlined,
} from '@ant-design/icons'
import * as questionApi from '../api/question'
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: '#1677ff',
description: '基础知识考察',
},
{
key: 'multiple-selection',
title: '多选题',
icon: <UnorderedListOutlined />,
color: '#52c41a',
description: '综合能力提升',
},
{
key: 'true-false',
title: '判断题',
icon: <CheckCircleOutlined />,
color: '#fa8c16',
description: '快速判断训练',
},
{
key: 'fill-in-blank',
title: '填空题',
icon: <FileTextOutlined />,
color: '#722ed1',
description: '填空补充练习',
},
{
key: 'short-answer',
title: '简答题',
icon: <EditOutlined />,
color: '#eb2f96',
description: '深度理解练习',
},
]
interface UserInfo {
username: string
nickname: string
avatar: string
}
const Home: React.FC = () => {
const navigate = useNavigate()
const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
const [statistics, setStatistics] = useState<Statistics>({
total_questions: 0,
answered_questions: 0,
correct_answers: 0,
accuracy: 0,
})
// 加载统计数据
const loadStatistics = async () => {
try {
const res = await questionApi.getStatistics()
if (res.success && res.data) {
setStatistics(res.data)
}
} catch (error) {
console.error('加载统计失败:', error)
}
}
// 加载用户信息
useEffect(() => {
const token = localStorage.getItem('token')
const savedUserInfo = localStorage.getItem('user')
if (token && savedUserInfo) {
try {
setUserInfo(JSON.parse(savedUserInfo))
} catch (e) {
console.error('解析用户信息失败', e)
}
}
}, [])
useEffect(() => {
loadStatistics()
}, [])
// 点击题型卡片
const handleTypeClick = async (type: string) => {
try {
// 加载该题型的题目列表
const res = await questionApi.getQuestions({ type })
if (res.success && res.data && res.data.length > 0) {
// 跳转到答题页面,并传递题型参数
navigate(`/question?type=${type}`)
} else {
message.warning('该题型暂无题目')
}
} catch (error) {
message.error('加载题目失败')
}
}
// 退出登录
const handleLogout = () => {
Modal.confirm({
title: '确定要退出登录吗?',
onOk: () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
setUserInfo(null)
message.success('已退出登录')
navigate('/login')
},
})
}
return (
<div className={styles.container}>
{/* 头部 */}
<div className={styles.header}>
<div className={styles.headerLeft}>
<Title level={2} className={styles.title}>AnKao </Title>
<Paragraph className={styles.subtitle}></Paragraph>
</div>
{/* 用户信息 */}
{userInfo && (
<div className={styles.userInfo}>
<Space size="middle">
<Avatar
src={userInfo.avatar || undefined}
size={40}
icon={<UserOutlined />}
/>
<div className={styles.userDetails}>
<Text strong className={styles.userNickname}>{userInfo.nickname}</Text>
<Text type="secondary" className={styles.userUsername}>@{userInfo.username}</Text>
</div>
<Button
type="text"
danger
icon={<LogoutOutlined />}
onClick={handleLogout}
>
退
</Button>
</Space>
</div>
)}
</div>
{/* 统计卡片 */}
<Card className={styles.statsCard}>
<Row gutter={[16, 16]}>
<Col xs={8} sm={8} md={8}>
<Statistic
title="题库总数"
value={statistics.total_questions}
valueStyle={{ color: '#1677ff', fontSize: '24px' }}
/>
</Col>
<Col xs={8} sm={8} md={8}>
<Statistic
title="已答题数"
value={statistics.answered_questions}
valueStyle={{ color: '#52c41a', fontSize: '24px' }}
/>
</Col>
<Col xs={8} sm={8} md={8}>
<Statistic
title="正确率"
value={statistics.accuracy.toFixed(0)}
suffix="%"
valueStyle={{ color: '#fa8c16', fontSize: '24px' }}
/>
</Col>
</Row>
</Card>
{/* 题型选择 */}
<div className={styles.typeSection}>
<Title level={4} className={styles.sectionTitle}>
<FileTextOutlined />
</Title>
<Row gutter={[16, 16]}>
{questionTypes.map(type => (
<Col xs={24} sm={12} md={8} lg={8} key={type.key}>
<Card
hoverable
className={styles.typeCard}
onClick={() => handleTypeClick(type.key)}
styles={{
body: {
textAlign: 'center',
padding: '24px',
}
}}
>
<div className={styles.typeIcon} style={{ color: type.color, fontSize: '48px' }}>
{type.icon}
</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}>
<Card
hoverable
className={styles.quickCard}
onClick={() => navigate('/question')}
>
<Space align="center" size="large">
<div className={styles.quickIcon}>
<RocketOutlined style={{ fontSize: '32px', color: '#722ed1' }} />
</div>
<div>
<Title level={5} style={{ margin: 0 }}></Title>
<Paragraph type="secondary" style={{ margin: 0 }}></Paragraph>
</div>
</Space>
</Card>
<Card
hoverable
className={styles.quickCard}
onClick={() => navigate('/wrong-questions')}
style={{ marginTop: '16px' }}
>
<Space align="center" size="large">
<div className={styles.quickIcon}>
<BookOutlined style={{ fontSize: '32px', color: '#ff4d4f' }} />
</div>
<div>
<Title level={5} style={{ margin: 0 }}></Title>
<Paragraph type="secondary" style={{ margin: 0 }}></Paragraph>
</div>
</Space>
</Card>
</div>
</div>
)
}
export default Home