cosmo/frontend/src/pages/admin/UserProfile.tsx

188 lines
5.7 KiB
TypeScript
Raw Normal View History

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>
);
}