完善
This commit is contained in:
@@ -55,12 +55,12 @@ const TodoHeader: React.FC<TodoHeaderProps> = () => {
|
||||
<div style={{ padding: '0' }}>
|
||||
<Text strong style={{ fontSize: '24px' }}>Todo List</Text>
|
||||
</div>
|
||||
<Space>
|
||||
<Space style={{ cursor: 'pointer' }} onClick={() => navigate('/profile')}>
|
||||
<Avatar style={{ backgroundColor: avatarBgColor, verticalAlign: 'middle' }} size="large">
|
||||
{userName.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Text>{userName}</Text>
|
||||
<Button type="text" danger icon={<LogoutOutlined />} onClick={showConfirmLogout}>
|
||||
<Button type="text" danger icon={<LogoutOutlined />} onClick={(e) => { e.stopPropagation(); showConfirmLogout(); }}>
|
||||
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
@@ -5,6 +5,7 @@ import App from './App.tsx';
|
||||
import Login from './pages/Login';
|
||||
import Register from './pages/Register';
|
||||
import ForgotPassword from './pages/ForgotPassword';
|
||||
import Profile from './pages/Profile';
|
||||
import 'antd/dist/reset.css';
|
||||
import '@ant-design/v5-patch-for-react-19';
|
||||
|
||||
@@ -15,6 +16,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="/*" element={<App />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -19,11 +19,12 @@ function ForgotPassword() {
|
||||
try {
|
||||
// 模拟API调用,实际情况会调用后端接口发送重置邮件
|
||||
console.log('Reset password request for:', values.email);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟网络延迟
|
||||
// await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟网络延迟
|
||||
|
||||
setMessage('如果您的账户存在,密码重置链接已发送到您的邮箱。');
|
||||
// setMessage('如果您的账户存在,密码重置链接已发送到您的邮箱。');
|
||||
// 实际情况下,这里可能会调用后端接口,例如:
|
||||
// await api.post('/auth/forgot-password', { email: values.email });
|
||||
await api.post('/auth/forgot-password', { email: values.email });
|
||||
setMessage('如果您的账户存在,密码重置链接已发送到您的邮箱。');
|
||||
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.message || '请求失败,请稍后再试。';
|
||||
|
||||
@@ -20,7 +20,7 @@ function Login() {
|
||||
password: values.password,
|
||||
});
|
||||
localStorage.setItem('token', response.data.token);
|
||||
localStorage.setItem('username', values.username);
|
||||
localStorage.setItem('username', response.data.username);
|
||||
navigate('/');
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.message || '登录失败,请检查用户名或密码。';
|
||||
@@ -72,11 +72,11 @@ function Login() {
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入用户名!' }]}
|
||||
rules={[{ required: true, message: '请输入用户名/邮箱!' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined className="site-form-item-icon" />}
|
||||
placeholder="账户"
|
||||
placeholder="账户/邮箱"
|
||||
size="large"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
191
client/src/pages/Profile.tsx
Normal file
191
client/src/pages/Profile.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Input, Button, Card, Space, Typography, Avatar, notification, Tabs } from 'antd';
|
||||
import { UserOutlined, MailOutlined, LockOutlined, ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { api } from '../api/axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface UserProfile {
|
||||
username: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const [passwordForm] = Form.useForm();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserProfile();
|
||||
}, []);
|
||||
|
||||
const fetchUserProfile = async () => {
|
||||
try {
|
||||
const response = await api.get('/auth/profile');
|
||||
setProfile(response.data);
|
||||
form.setFieldsValue(response.data);
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
message: '获取用户信息失败',
|
||||
description: error.response?.data?.message || '请稍后再试。',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdateProfile = async (values: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.put('/auth/profile', { email: values.email });
|
||||
setProfile(response.data);
|
||||
notification.success({
|
||||
message: response.data.message || '用户信息更新成功',
|
||||
});
|
||||
fetchUserProfile();
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
message: '更新用户信息失败',
|
||||
description: error.response?.data?.message || '请稍后再试。',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onChangePassword = async (values: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.put('/auth/change-password', { oldPassword: values.oldPassword, newPassword: values.newPassword });
|
||||
notification.success({
|
||||
message: '密码修改成功',
|
||||
});
|
||||
passwordForm.resetFields();
|
||||
} catch (error: any) {
|
||||
notification.error({
|
||||
message: '密码修改失败',
|
||||
description: error.response?.data?.message || '请稍后再试。',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
key: '1',
|
||||
label: '基本信息',
|
||||
children: (
|
||||
<Form
|
||||
form={form}
|
||||
name="profile_info"
|
||||
onFinish={onUpdateProfile}
|
||||
autoComplete="off"
|
||||
layout="vertical"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Form.Item label="用户名" name="username">
|
||||
<Input prefix={<UserOutlined />} disabled />
|
||||
</Form.Item>
|
||||
<Form.Item label="邮箱" name="email" rules={profile && profile.email ? [] : [{ required: true, message: '请输入您的邮箱!' }, { type: 'email', message: '请输入有效的邮箱地址!' }]}>
|
||||
<Input prefix={<MailOutlined />} disabled={!!(profile && profile.email)} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" className="w-full" size="large" loading={loading}>
|
||||
保存信息
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '修改密码',
|
||||
children: (
|
||||
<Form
|
||||
form={passwordForm}
|
||||
name="change_password"
|
||||
onFinish={onChangePassword}
|
||||
autoComplete="off"
|
||||
layout="vertical"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Form.Item
|
||||
name="oldPassword"
|
||||
label="旧密码"
|
||||
rules={[{ required: true, message: '请输入您的旧密码!' }]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="newPassword"
|
||||
label="新密码"
|
||||
rules={[{ required: true, message: '请输入您的新密码!' }, { min: 6, message: '密码至少需要6位字符!' }]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="confirmNewPassword"
|
||||
label="确认新密码"
|
||||
dependencies={['newPassword']}
|
||||
hasFeedback
|
||||
rules={[
|
||||
{ required: true, message: '请确认您的新密码!' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('newPassword') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致!'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" className="w-full" size="large" loading={loading}>
|
||||
修改密码
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-100" style={{ background: '#f0f2f5', height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Card
|
||||
className="w-full max-w-lg"
|
||||
variant="borderless"
|
||||
style={{
|
||||
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)',
|
||||
padding: '24px 24px 24px',
|
||||
textAlign: 'center',
|
||||
width: '600px',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined style={{ fontSize: '20px' }} />}
|
||||
onClick={() => navigate('/')}
|
||||
style={{ position: 'absolute', top: 16, left: 16, zIndex: 1 }}
|
||||
/>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: 24 }}>
|
||||
<Avatar size={64} icon={<UserOutlined />} style={{ backgroundColor: '#87d068', marginBottom: 16 }} />
|
||||
<Title level={2} style={{ margin: '8px 0 0' }}>个人信息</Title>
|
||||
<Text type="secondary">管理您的账户设置</Text>
|
||||
</div>
|
||||
|
||||
<Tabs defaultActiveKey="1" items={tabItems} centered style={{ width: '100%' }} />
|
||||
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Form, Input, Button, Card, Space, Typography, Alert } from 'antd';
|
||||
import { UserOutlined, LockOutlined, CheckSquareOutlined } from '@ant-design/icons';
|
||||
import { UserOutlined, LockOutlined, CheckSquareOutlined, MailOutlined } from '@ant-design/icons';
|
||||
import { api } from '../api/axios';
|
||||
|
||||
const { Title, Text, Link } = Typography;
|
||||
@@ -17,6 +17,7 @@ function Register() {
|
||||
try {
|
||||
await api.post('/auth/register', {
|
||||
username: values.username,
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
});
|
||||
navigate('/login');
|
||||
@@ -79,6 +80,17 @@ function Register() {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[{ required: true, message: '请输入邮箱!' }, { type: 'email', message: '请输入有效的邮箱地址!' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<MailOutlined className="site-form-item-icon" />}
|
||||
placeholder="邮箱"
|
||||
size="large"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码!' }]}
|
||||
|
||||
Reference in New Issue
Block a user