- 实现登录页面: - 添加登录和注册表单切换功能 - 使用antd-mobile组件(Form, Input, Button, Toast) - 白色背景设计,标题使用主题色 - 表单验证和错误提示 - 已登录用户自动重定向 - 完善用户认证: - 路由保护,未登录用户重定向到登录页 - 用户信息保存到localStorage - Profile页面支持退出登录 - 后端改进: - 启用CORS中间件支持跨域请求 - 更新开发规范: - 在CLAUDE.md中添加前端开发规范 - 明确UI组件使用原则:优先使用antd-mobile组件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
233 lines
5.5 KiB
TypeScript
233 lines
5.5 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
||
import { useNavigate } from 'react-router-dom'
|
||
import { Button, Input, Form, Toast } from 'antd-mobile'
|
||
import styles from './Login.module.less'
|
||
|
||
interface LoginResponse {
|
||
success: boolean
|
||
message: string
|
||
data?: {
|
||
token: string
|
||
user: {
|
||
username: string
|
||
avatar: string
|
||
nickname: string
|
||
}
|
||
}
|
||
}
|
||
|
||
const Login: React.FC = () => {
|
||
const navigate = useNavigate()
|
||
const [isLogin, setIsLogin] = useState(true)
|
||
const [loading, setLoading] = useState(false)
|
||
|
||
// 表单字段
|
||
const [username, setUsername] = useState('')
|
||
const [password, setPassword] = useState('')
|
||
const [nickname, setNickname] = useState('')
|
||
|
||
// 如果已登录,重定向到首页
|
||
useEffect(() => {
|
||
const token = localStorage.getItem('token')
|
||
if (token) {
|
||
navigate('/', { replace: true })
|
||
}
|
||
}, [navigate])
|
||
|
||
// 处理登录
|
||
const handleLogin = async () => {
|
||
if (!username || !password) {
|
||
Toast.show({
|
||
icon: 'fail',
|
||
content: '请输入用户名和密码',
|
||
})
|
||
return
|
||
}
|
||
|
||
setLoading(true)
|
||
try {
|
||
const response = await fetch('http://localhost:8080/api/login', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ username, password }),
|
||
})
|
||
|
||
const data: LoginResponse = await response.json()
|
||
|
||
if (data.success && data.data) {
|
||
// 保存token到localStorage
|
||
localStorage.setItem('token', data.data.token)
|
||
localStorage.setItem('user', JSON.stringify(data.data.user))
|
||
|
||
Toast.show({
|
||
icon: 'success',
|
||
content: '登录成功',
|
||
})
|
||
|
||
// 跳转到首页
|
||
navigate('/')
|
||
} else {
|
||
Toast.show({
|
||
icon: 'fail',
|
||
content: data.message || '登录失败',
|
||
})
|
||
}
|
||
} catch (err) {
|
||
Toast.show({
|
||
icon: 'fail',
|
||
content: '网络错误,请稍后重试',
|
||
})
|
||
console.error('登录错误:', err)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
// 处理注册
|
||
const handleRegister = async () => {
|
||
if (!username || !password) {
|
||
Toast.show({
|
||
icon: 'fail',
|
||
content: '请输入用户名和密码',
|
||
})
|
||
return
|
||
}
|
||
|
||
if (password.length < 6) {
|
||
Toast.show({
|
||
icon: 'fail',
|
||
content: '密码长度至少6位',
|
||
})
|
||
return
|
||
}
|
||
|
||
setLoading(true)
|
||
try {
|
||
const response = await fetch('http://localhost:8080/api/register', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ username, password, nickname }),
|
||
})
|
||
|
||
const data: LoginResponse = await response.json()
|
||
|
||
if (data.success && data.data) {
|
||
// 保存token到localStorage
|
||
localStorage.setItem('token', data.data.token)
|
||
localStorage.setItem('user', JSON.stringify(data.data.user))
|
||
|
||
Toast.show({
|
||
icon: 'success',
|
||
content: '注册成功',
|
||
})
|
||
|
||
// 跳转到首页
|
||
navigate('/')
|
||
} else {
|
||
Toast.show({
|
||
icon: 'fail',
|
||
content: data.message || '注册失败',
|
||
})
|
||
}
|
||
} catch (err) {
|
||
Toast.show({
|
||
icon: 'fail',
|
||
content: '网络错误,请稍后重试',
|
||
})
|
||
console.error('注册错误:', err)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className={styles.container}>
|
||
<div className={styles.header}>
|
||
<p className={styles.subtitle}>欢迎使用AnKao题库系统</p>
|
||
<h1 className={styles.title}>AnKao</h1>
|
||
</div>
|
||
|
||
<Form
|
||
layout="vertical"
|
||
onFinish={isLogin ? handleLogin : handleRegister}
|
||
className={styles.form}
|
||
>
|
||
<Form.Item label="用户名">
|
||
<Input
|
||
value={username}
|
||
onChange={setUsername}
|
||
placeholder="请输入用户名"
|
||
disabled={loading}
|
||
clearable
|
||
/>
|
||
</Form.Item>
|
||
|
||
<Form.Item label="密码">
|
||
<Input
|
||
type="password"
|
||
value={password}
|
||
onChange={setPassword}
|
||
placeholder={isLogin ? '请输入密码' : '请输入密码(至少6位)'}
|
||
disabled={loading}
|
||
clearable
|
||
/>
|
||
</Form.Item>
|
||
|
||
{!isLogin && (
|
||
<Form.Item label="昵称(可选)">
|
||
<Input
|
||
value={nickname}
|
||
onChange={setNickname}
|
||
placeholder="请输入昵称"
|
||
disabled={loading}
|
||
clearable
|
||
/>
|
||
</Form.Item>
|
||
)}
|
||
|
||
<Button
|
||
type="submit"
|
||
color="primary"
|
||
block
|
||
loading={loading}
|
||
disabled={loading}
|
||
>
|
||
{isLogin ? '登录' : '注册'}
|
||
</Button>
|
||
</Form>
|
||
|
||
<div className={styles.switchMode}>
|
||
{isLogin ? (
|
||
<p>
|
||
还没有账号?
|
||
<button
|
||
onClick={() => setIsLogin(false)}
|
||
className={styles.linkBtn}
|
||
disabled={loading}
|
||
>
|
||
立即注册
|
||
</button>
|
||
</p>
|
||
) : (
|
||
<p>
|
||
已有账号?
|
||
<button
|
||
onClick={() => setIsLogin(true)}
|
||
className={styles.linkBtn}
|
||
disabled={loading}
|
||
>
|
||
立即登录
|
||
</button>
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default Login
|