AnCao/web/src/pages/Login.tsx
yanlongqi 92e8c9a94c 实现完整的登录注册功能并优化前端组件
- 实现登录页面:
  - 添加登录和注册表单切换功能
  - 使用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>
2025-11-03 23:56:53 +08:00

233 lines
5.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, { 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