优化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:
parent
6446508954
commit
9ca8f123e7
@ -3,7 +3,6 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
|
||||
import TabBarLayout from './components/TabBarLayout'
|
||||
import ProtectedRoute from './components/ProtectedRoute'
|
||||
import QuestionPage from './pages/Question'
|
||||
import Profile from './pages/Profile'
|
||||
import Login from './pages/Login'
|
||||
import Home from './pages/Home'
|
||||
import About from './pages/About'
|
||||
@ -17,7 +16,6 @@ const App: React.FC = () => {
|
||||
<Route element={<ProtectedRoute><TabBarLayout /></ProtectedRoute>}>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/question" element={<QuestionPage />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
</Route>
|
||||
|
||||
{/* 不带TabBar的页面,但需要登录保护 */}
|
||||
|
||||
@ -4,7 +4,6 @@ import { Layout, Menu } from 'antd'
|
||||
import {
|
||||
HomeOutlined,
|
||||
FileTextOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import styles from './TabBarLayout.module.less'
|
||||
|
||||
@ -35,11 +34,6 @@ const TabBarLayout: React.FC = () => {
|
||||
icon: <FileTextOutlined />,
|
||||
label: '答题',
|
||||
},
|
||||
{
|
||||
key: '/profile',
|
||||
icon: <UserOutlined />,
|
||||
label: '我的',
|
||||
},
|
||||
]
|
||||
|
||||
const handleMenuClick = (key: string) => {
|
||||
|
||||
@ -6,10 +6,16 @@
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 32px;
|
||||
color: white;
|
||||
|
||||
.headerLeft {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: white !important;
|
||||
margin-bottom: 8px !important;
|
||||
@ -19,6 +25,30 @@
|
||||
.subtitle {
|
||||
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 {
|
||||
@ -85,11 +115,21 @@
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.headerLeft {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px !important;
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.statsCard {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
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 {
|
||||
FileTextOutlined,
|
||||
CheckCircleOutlined,
|
||||
@ -8,12 +8,14 @@ import {
|
||||
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 } = Typography
|
||||
const { Title, Paragraph, Text } = Typography
|
||||
|
||||
// 题型配置
|
||||
const questionTypes = [
|
||||
@ -54,8 +56,15 @@ const questionTypes = [
|
||||
},
|
||||
]
|
||||
|
||||
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,
|
||||
@ -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(() => {
|
||||
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 (
|
||||
<div className={styles.container}>
|
||||
{/* 头部 */}
|
||||
<div className={styles.header}>
|
||||
<Title level={2} className={styles.title}>AnKao 刷题</Title>
|
||||
<Paragraph className={styles.subtitle}>选择题型开始练习</Paragraph>
|
||||
<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>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@ -17,6 +17,7 @@ import {
|
||||
Typography,
|
||||
Row,
|
||||
Col,
|
||||
Spin,
|
||||
} from 'antd'
|
||||
import {
|
||||
CheckOutlined,
|
||||
@ -25,6 +26,7 @@ import {
|
||||
UnorderedListOutlined,
|
||||
FilterOutlined,
|
||||
ReloadOutlined,
|
||||
ArrowLeftOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import type { Question, AnswerResult } from '../types/question'
|
||||
import * as questionApi from '../api/question'
|
||||
@ -35,11 +37,13 @@ const { Title, Text } = Typography
|
||||
|
||||
const QuestionPage: React.FC = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
const navigate = useNavigate()
|
||||
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null)
|
||||
const [selectedAnswer, setSelectedAnswer] = useState<string | string[]>('')
|
||||
const [showResult, setShowResult] = useState(false)
|
||||
const [answerResult, setAnswerResult] = useState<AnswerResult | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [autoNextLoading, setAutoNextLoading] = useState(false)
|
||||
const [allQuestions, setAllQuestions] = useState<Question[]>([])
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const [fillAnswers, setFillAnswers] = useState<string[]>([])
|
||||
@ -148,10 +152,13 @@ const QuestionPage: React.FC = () => {
|
||||
setAnswerResult(res.data)
|
||||
setShowResult(true)
|
||||
|
||||
// 如果答案正确,1秒后自动进入下一题
|
||||
if (res.data.correct) {
|
||||
message.success('回答正确!')
|
||||
} else {
|
||||
message.error('回答错误')
|
||||
setAutoNextLoading(true)
|
||||
setTimeout(() => {
|
||||
setAutoNextLoading(false)
|
||||
handleNext()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -357,6 +364,12 @@ const QuestionPage: React.FC = () => {
|
||||
<div className={styles.content}>
|
||||
{/* 头部 */}
|
||||
<div className={styles.header}>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
返回首页
|
||||
</Button>
|
||||
<Title level={3} className={styles.title}>AnKao 刷题</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
@ -458,8 +471,14 @@ const QuestionPage: React.FC = () => {
|
||||
提交答案
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="primary" size="large" block onClick={handleNext}>
|
||||
下一题
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
onClick={handleNext}
|
||||
loading={autoNextLoading}
|
||||
>
|
||||
{autoNextLoading ? '正在跳转到下一题...' : '下一题'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user