优化UI交互体验:整合用户中心到首页并改进答题流程

- 删除独立的Profile页面,将用户信息整合到Home页面顶部
- 在首页添加用户头像、昵称、用户名显示和退出登录按钮
- 优化响应式布局:移动端和PC端用户信息区域自适应
- 答题页面新增返回首页按钮,方便导航
- 实现答对自动跳转:答案正确后1秒自动进入下一题
- 更新底部导航栏:移除"我的"选项,保留首页和答题
- 调整Header布局为flex布局,支持左右分区显示

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
yanlongqi 2025-11-04 14:29:40 +08:00
parent 6446508954
commit 9ca8f123e7
7 changed files with 133 additions and 246 deletions

View File

@ -3,7 +3,6 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import TabBarLayout from './components/TabBarLayout' import TabBarLayout from './components/TabBarLayout'
import ProtectedRoute from './components/ProtectedRoute' import ProtectedRoute from './components/ProtectedRoute'
import QuestionPage from './pages/Question' import QuestionPage from './pages/Question'
import Profile from './pages/Profile'
import Login from './pages/Login' import Login from './pages/Login'
import Home from './pages/Home' import Home from './pages/Home'
import About from './pages/About' import About from './pages/About'
@ -17,7 +16,6 @@ const App: React.FC = () => {
<Route element={<ProtectedRoute><TabBarLayout /></ProtectedRoute>}> <Route element={<ProtectedRoute><TabBarLayout /></ProtectedRoute>}>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/question" element={<QuestionPage />} /> <Route path="/question" element={<QuestionPage />} />
<Route path="/profile" element={<Profile />} />
</Route> </Route>
{/* 不带TabBar的页面但需要登录保护 */} {/* 不带TabBar的页面但需要登录保护 */}

View File

@ -4,7 +4,6 @@ import { Layout, Menu } from 'antd'
import { import {
HomeOutlined, HomeOutlined,
FileTextOutlined, FileTextOutlined,
UserOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import styles from './TabBarLayout.module.less' import styles from './TabBarLayout.module.less'
@ -35,11 +34,6 @@ const TabBarLayout: React.FC = () => {
icon: <FileTextOutlined />, icon: <FileTextOutlined />,
label: '答题', label: '答题',
}, },
{
key: '/profile',
icon: <UserOutlined />,
label: '我的',
},
] ]
const handleMenuClick = (key: string) => { const handleMenuClick = (key: string) => {

View File

@ -6,10 +6,16 @@
} }
.header { .header {
text-align: center; display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 32px; margin-bottom: 32px;
color: white; color: white;
.headerLeft {
flex: 1;
}
.title { .title {
color: white !important; color: white !important;
margin-bottom: 8px !important; margin-bottom: 8px !important;
@ -19,6 +25,30 @@
.subtitle { .subtitle {
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
} }
.userInfo {
background: rgba(255, 255, 255, 0.15);
padding: 12px 20px;
border-radius: 12px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
.userDetails {
display: flex;
flex-direction: column;
gap: 2px;
.userNickname {
color: white !important;
font-size: 16px;
}
.userUsername {
color: rgba(255, 255, 255, 0.8) !important;
font-size: 12px;
}
}
}
} }
.statsCard { .statsCard {
@ -85,11 +115,21 @@
} }
.header { .header {
flex-direction: column;
align-items: stretch;
margin-bottom: 24px; margin-bottom: 24px;
.headerLeft {
margin-bottom: 16px;
}
.title { .title {
font-size: 24px !important; font-size: 24px !important;
} }
.userInfo {
width: 100%;
}
} }
.statsCard { .statsCard {

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Card, Statistic, Row, Col, Typography, message, Space } from 'antd' import { Card, Statistic, Row, Col, Typography, message, Space, Avatar, Button, Modal } from 'antd'
import { import {
FileTextOutlined, FileTextOutlined,
CheckCircleOutlined, CheckCircleOutlined,
@ -8,12 +8,14 @@ import {
EditOutlined, EditOutlined,
RocketOutlined, RocketOutlined,
BookOutlined, BookOutlined,
UserOutlined,
LogoutOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import * as questionApi from '../api/question' import * as questionApi from '../api/question'
import type { Statistics } from '../types/question' import type { Statistics } from '../types/question'
import styles from './Home.module.less' import styles from './Home.module.less'
const { Title, Paragraph } = Typography const { Title, Paragraph, Text } = Typography
// 题型配置 // 题型配置
const questionTypes = [ const questionTypes = [
@ -54,8 +56,15 @@ const questionTypes = [
}, },
] ]
interface UserInfo {
username: string
nickname: string
avatar: string
}
const Home: React.FC = () => { const Home: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
const [statistics, setStatistics] = useState<Statistics>({ const [statistics, setStatistics] = useState<Statistics>({
total_questions: 0, total_questions: 0,
answered_questions: 0, answered_questions: 0,
@ -75,6 +84,20 @@ const Home: React.FC = () => {
} }
} }
// 加载用户信息
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(() => { useEffect(() => {
loadStatistics() loadStatistics()
}, []) }, [])
@ -96,12 +119,52 @@ const Home: React.FC = () => {
} }
} }
// 退出登录
const handleLogout = () => {
Modal.confirm({
title: '确定要退出登录吗?',
onOk: () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
setUserInfo(null)
message.success('已退出登录')
navigate('/login')
},
})
}
return ( return (
<div className={styles.container}> <div className={styles.container}>
{/* 头部 */} {/* 头部 */}
<div className={styles.header}> <div className={styles.header}>
<Title level={2} className={styles.title}>AnKao </Title> <div className={styles.headerLeft}>
<Paragraph className={styles.subtitle}></Paragraph> <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> </div>
{/* 统计卡片 */} {/* 统计卡片 */}

View File

@ -1,79 +0,0 @@
.container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 24px;
padding-bottom: 80px;
}
.content {
max-width: 600px;
margin: 0 auto;
}
.userCard {
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
margin-bottom: 24px;
}
.userInfo {
display: flex;
align-items: center;
gap: 20px;
}
.userDetails {
flex: 1;
}
.userNickname {
margin: 0 !important;
}
.userUsername {
display: block;
margin-top: 4px;
}
.menuCard {
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
margin-bottom: 24px;
}
.logoutButton {
height: 48px;
font-size: 16px;
font-weight: 500;
border-radius: 12px;
}
// 响应式设计 - 移动端
@media (max-width: 768px) {
.container {
padding: 16px;
padding-bottom: 70px;
}
.userCard,
.menuCard {
border-radius: 12px;
margin-bottom: 16px;
}
.userInfo {
gap: 16px;
}
.logoutButton {
height: 44px;
font-size: 15px;
}
}
// 响应式设计 - PC端
@media (min-width: 769px) {
.container {
padding: 32px;
}
}

View File

@ -1,148 +0,0 @@
import React, { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import {
Card,
Avatar,
List,
Button,
Modal,
message,
Typography,
Space,
} from 'antd'
import {
RightOutlined,
SettingOutlined,
FileTextOutlined,
UserOutlined,
BookOutlined,
} from '@ant-design/icons'
import styles from './Profile.module.less'
const { Title, Text } = Typography
interface UserInfo {
username: string
nickname: string
avatar: string
}
const Profile: React.FC = () => {
const navigate = useNavigate()
const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
useEffect(() => {
// 从 localStorage 获取用户信息
const token = localStorage.getItem('token')
const savedUserInfo = localStorage.getItem('user')
if (token && savedUserInfo) {
try {
setUserInfo(JSON.parse(savedUserInfo))
} catch (e) {
console.error('解析用户信息失败', e)
}
}
}, [])
const handleLogout = () => {
Modal.confirm({
title: '确定要退出登录吗?',
onOk: () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
setUserInfo(null)
message.success('已退出登录')
navigate('/login')
},
})
}
const handleLogin = () => {
navigate('/login')
}
return (
<div className={styles.container}>
<div className={styles.content}>
{/* 用户信息卡片 */}
<Card className={styles.userCard}>
{userInfo ? (
<div className={styles.userInfo}>
<Avatar
src={userInfo.avatar || undefined}
size={80}
icon={<UserOutlined />}
/>
<div className={styles.userDetails}>
<Title level={4} className={styles.userNickname}>{userInfo.nickname}</Title>
<Text type="secondary" className={styles.userUsername}>@{userInfo.username}</Text>
</div>
</div>
) : (
<div className={styles.userInfo}>
<Avatar size={80} icon={<UserOutlined />} />
<div className={styles.userDetails}>
<Title level={4} className={styles.userNickname}></Title>
<Button type="primary" onClick={handleLogin} style={{ marginTop: 8 }}>
</Button>
</div>
</div>
)}
</Card>
{/* 功能列表 */}
<Card title="功能" className={styles.menuCard}>
<List>
<List.Item
onClick={() => navigate('/wrong-questions')}
style={{ cursor: 'pointer' }}
>
<Space>
<BookOutlined />
<span></span>
</Space>
<RightOutlined />
</List.Item>
<List.Item
onClick={() => message.info('功能开发中')}
style={{ cursor: 'pointer' }}
>
<Space>
<FileTextOutlined />
<span></span>
</Space>
<RightOutlined />
</List.Item>
<List.Item
onClick={() => message.info('功能开发中')}
style={{ cursor: 'pointer' }}
>
<Space>
<SettingOutlined />
<span></span>
</Space>
<RightOutlined />
</List.Item>
</List>
</Card>
{/* 退出登录按钮 */}
{userInfo && (
<Button
danger
block
size="large"
onClick={handleLogout}
className={styles.logoutButton}
>
退
</Button>
)}
</div>
</div>
)
}
export default Profile

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { useSearchParams } from 'react-router-dom' import { useSearchParams, useNavigate } from 'react-router-dom'
import { import {
Button, Button,
Card, Card,
@ -17,6 +17,7 @@ import {
Typography, Typography,
Row, Row,
Col, Col,
Spin,
} from 'antd' } from 'antd'
import { import {
CheckOutlined, CheckOutlined,
@ -25,6 +26,7 @@ import {
UnorderedListOutlined, UnorderedListOutlined,
FilterOutlined, FilterOutlined,
ReloadOutlined, ReloadOutlined,
ArrowLeftOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import type { Question, AnswerResult } from '../types/question' import type { Question, AnswerResult } from '../types/question'
import * as questionApi from '../api/question' import * as questionApi from '../api/question'
@ -35,11 +37,13 @@ const { Title, Text } = Typography
const QuestionPage: React.FC = () => { const QuestionPage: React.FC = () => {
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const navigate = useNavigate()
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null) const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null)
const [selectedAnswer, setSelectedAnswer] = useState<string | string[]>('') const [selectedAnswer, setSelectedAnswer] = useState<string | string[]>('')
const [showResult, setShowResult] = useState(false) const [showResult, setShowResult] = useState(false)
const [answerResult, setAnswerResult] = useState<AnswerResult | null>(null) const [answerResult, setAnswerResult] = useState<AnswerResult | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [autoNextLoading, setAutoNextLoading] = useState(false)
const [allQuestions, setAllQuestions] = useState<Question[]>([]) const [allQuestions, setAllQuestions] = useState<Question[]>([])
const [currentIndex, setCurrentIndex] = useState(0) const [currentIndex, setCurrentIndex] = useState(0)
const [fillAnswers, setFillAnswers] = useState<string[]>([]) const [fillAnswers, setFillAnswers] = useState<string[]>([])
@ -148,10 +152,13 @@ const QuestionPage: React.FC = () => {
setAnswerResult(res.data) setAnswerResult(res.data)
setShowResult(true) setShowResult(true)
// 如果答案正确1秒后自动进入下一题
if (res.data.correct) { if (res.data.correct) {
message.success('回答正确!') setAutoNextLoading(true)
} else { setTimeout(() => {
message.error('回答错误') setAutoNextLoading(false)
handleNext()
}, 1000)
} }
} }
} catch (error) { } catch (error) {
@ -357,6 +364,12 @@ const QuestionPage: React.FC = () => {
<div className={styles.content}> <div className={styles.content}>
{/* 头部 */} {/* 头部 */}
<div className={styles.header}> <div className={styles.header}>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/')}
>
</Button>
<Title level={3} className={styles.title}>AnKao </Title> <Title level={3} className={styles.title}>AnKao </Title>
<Button <Button
type="primary" type="primary"
@ -458,8 +471,14 @@ const QuestionPage: React.FC = () => {
</Button> </Button>
) : ( ) : (
<Button type="primary" size="large" block onClick={handleNext}> <Button
type="primary"
size="large"
block
onClick={handleNext}
loading={autoNextLoading}
>
{autoNextLoading ? '正在跳转到下一题...' : '下一题'}
</Button> </Button>
)} )}
</div> </div>