2025-12-26 01:21:15 +00:00
|
|
|
/**
|
|
|
|
|
* User Profile Page
|
|
|
|
|
* 个人信息页面
|
|
|
|
|
*/
|
|
|
|
|
import { useState, useEffect } from 'react';
|
2026-01-18 02:54:03 +00:00
|
|
|
import { Form, Input, Button, Card, Avatar, Space, Descriptions, Row, Col, Upload, message } from 'antd';
|
|
|
|
|
import { UserOutlined, MailOutlined, IdcardOutlined, UploadOutlined } from '@ant-design/icons';
|
|
|
|
|
import { request, API_BASE_URL } from '../../utils/request';
|
2025-12-26 01:21:15 +00:00
|
|
|
import { auth } from '../../utils/auth';
|
|
|
|
|
import { useToast } from '../../contexts/ToastContext';
|
|
|
|
|
|
|
|
|
|
export function UserProfile() {
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
2026-01-18 02:54:03 +00:00
|
|
|
const [uploading, setUploading] = useState(false);
|
2025-12-26 01:21:15 +00:00
|
|
|
const [userProfile, setUserProfile] = useState<any>(null);
|
|
|
|
|
const toast = useToast();
|
|
|
|
|
const user = auth.getUser();
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadUserProfile();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const loadUserProfile = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await request.get('/users/me');
|
|
|
|
|
setUserProfile(data);
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
email: data.email || '',
|
|
|
|
|
full_name: data.full_name || '',
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast.error('获取用户信息失败');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async (values: any) => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
await request.put('/users/me/profile', {
|
|
|
|
|
full_name: values.full_name,
|
|
|
|
|
email: values.email || null,
|
|
|
|
|
});
|
|
|
|
|
toast.success('个人信息更新成功');
|
|
|
|
|
|
|
|
|
|
// Update local user info
|
|
|
|
|
const updatedUser = { ...user, full_name: values.full_name, email: values.email };
|
|
|
|
|
auth.setUser(updatedUser);
|
|
|
|
|
|
|
|
|
|
// Reload profile
|
|
|
|
|
await loadUserProfile();
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
toast.error(error.response?.data?.detail || '更新失败');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-18 02:54:03 +00:00
|
|
|
const handleAvatarUpload = async (options: any) => {
|
|
|
|
|
const { file } = options;
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('file', file);
|
|
|
|
|
|
|
|
|
|
setUploading(true);
|
|
|
|
|
try {
|
|
|
|
|
const { data } = await request.post('/users/me/avatar', formData, {
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'multipart/form-data',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
toast.success('头像上传成功');
|
|
|
|
|
|
|
|
|
|
// Update local user info with new avatar URL
|
|
|
|
|
auth.setUser({ ...user, avatar_url: data.avatar_url });
|
|
|
|
|
|
|
|
|
|
// Reload profile to get new avatar URL from backend
|
|
|
|
|
await loadUserProfile();
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
toast.error(error.response?.data?.detail || '头像上传失败');
|
|
|
|
|
} finally {
|
|
|
|
|
setUploading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Construct full avatar URL
|
|
|
|
|
const getAvatarUrl = () => {
|
|
|
|
|
if (!userProfile?.avatar_url) return null;
|
2026-01-18 05:33:50 +00:00
|
|
|
// Use relative path to allow proxying (Vite/Nginx)
|
2026-01-18 02:54:03 +00:00
|
|
|
// The backend returns a relative path like "user/1/avatar/avatar.png"
|
2026-01-18 05:33:50 +00:00
|
|
|
return `/upload/${userProfile.avatar_url}?t=${new Date().getTime()}`;
|
2026-01-18 02:54:03 +00:00
|
|
|
};
|
|
|
|
|
|
2025-12-26 01:21:15 +00:00
|
|
|
return (
|
|
|
|
|
<Row gutter={24}>
|
|
|
|
|
<Col span={8}>
|
|
|
|
|
{/* User Avatar and Basic Info Card */}
|
|
|
|
|
<Card bordered={false} loading={loading}>
|
|
|
|
|
<div style={{ textAlign: 'center' }}>
|
2026-01-18 02:54:03 +00:00
|
|
|
<div style={{ position: 'relative', display: 'inline-block' }}>
|
|
|
|
|
<Avatar
|
|
|
|
|
size={100}
|
|
|
|
|
src={getAvatarUrl()}
|
|
|
|
|
icon={<UserOutlined />}
|
|
|
|
|
/>
|
|
|
|
|
<div style={{ marginTop: 16 }}>
|
|
|
|
|
<Upload
|
|
|
|
|
name="avatar"
|
|
|
|
|
showUploadList={false}
|
|
|
|
|
customRequest={handleAvatarUpload}
|
|
|
|
|
accept="image/*"
|
|
|
|
|
>
|
|
|
|
|
<Button icon={<UploadOutlined />} size="small" loading={uploading}>
|
|
|
|
|
修改头像
|
|
|
|
|
</Button>
|
|
|
|
|
</Upload>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-26 01:21:15 +00:00
|
|
|
<h2 style={{ marginTop: 24, marginBottom: 8 }}>
|
|
|
|
|
{userProfile?.full_name || userProfile?.username || '用户'}
|
|
|
|
|
</h2>
|
|
|
|
|
<p style={{ color: '#999', marginBottom: 24 }}>
|
|
|
|
|
@{userProfile?.username}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
{userProfile && (
|
|
|
|
|
<Descriptions column={1} size="small">
|
|
|
|
|
<Descriptions.Item label="角色">
|
|
|
|
|
{userProfile.role === 'admin' ? '管理员' : '普通用户'}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="创建时间">
|
|
|
|
|
{new Date(userProfile.created_at).toLocaleString('zh-CN')}
|
|
|
|
|
</Descriptions.Item>
|
|
|
|
|
</Descriptions>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
|
|
|
<Col span={16}>
|
|
|
|
|
{/* Edit Profile Form */}
|
|
|
|
|
<Card title="编辑个人信息" bordered={false}>
|
|
|
|
|
<Form
|
|
|
|
|
form={form}
|
|
|
|
|
layout="vertical"
|
|
|
|
|
onFinish={handleSubmit}
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
>
|
|
|
|
|
<Form.Item
|
|
|
|
|
label="姓名"
|
|
|
|
|
name="full_name"
|
|
|
|
|
rules={[{ required: true, message: '请输入姓名' }]}
|
|
|
|
|
>
|
|
|
|
|
<Input
|
|
|
|
|
prefix={<IdcardOutlined />}
|
|
|
|
|
placeholder="请输入您的姓名"
|
|
|
|
|
size="large"
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
label="邮箱"
|
|
|
|
|
name="email"
|
|
|
|
|
rules={[
|
|
|
|
|
{ type: 'email', message: '请输入有效的邮箱地址' },
|
|
|
|
|
]}
|
|
|
|
|
>
|
|
|
|
|
<Input
|
|
|
|
|
prefix={<MailOutlined />}
|
|
|
|
|
placeholder="请输入邮箱地址(可选)"
|
|
|
|
|
size="large"
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
<Form.Item>
|
|
|
|
|
<Button type="primary" htmlType="submit" loading={loading} size="large" block>
|
|
|
|
|
保存修改
|
|
|
|
|
</Button>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Card>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
);
|
|
|
|
|
}
|