nex_docus/frontend/src/pages/Profile/ProfilePage.jsx

307 lines
9.0 KiB
React
Raw Normal View History

2025-12-20 11:18:59 +00:00
import { useState, useEffect } from 'react'
import { Card, Tabs, Form, Input, Button, Avatar, Upload, message } from 'antd'
import { UserOutlined, LockOutlined, UploadOutlined } from '@ant-design/icons'
2026-01-13 13:21:47 +00:00
import ImgCrop from 'antd-img-crop'
import { getCurrentUser, updateProfile, changePassword, uploadAvatar } from '@/api/auth'
2025-12-20 11:18:59 +00:00
import useUserStore from '@/stores/userStore'
import Toast from '@/components/Toast/Toast'
import './ProfilePage.css'
function ProfilePage() {
const [loading, setLoading] = useState(false)
const [userInfo, setUserInfo] = useState(null)
const [profileForm] = Form.useForm()
const [passwordForm] = Form.useForm()
const { user, setUser } = useUserStore()
useEffect(() => {
loadUserInfo()
}, [])
// 加载用户信息
const loadUserInfo = async () => {
try {
const res = await getCurrentUser()
setUserInfo(res.data)
profileForm.setFieldsValue({
username: res.data.username,
nickname: res.data.nickname,
email: res.data.email,
phone: res.data.phone,
})
} catch (error) {
console.error('Load user info error:', error)
message.error('加载用户信息失败')
}
}
// 更新资料
const handleUpdateProfile = async (values) => {
setLoading(true)
try {
const res = await updateProfile({
nickname: values.nickname,
email: values.email,
phone: values.phone,
})
setUserInfo(res.data)
setUser(res.data) // 更新全局用户信息
Toast.success('更新成功', '个人资料已更新')
} catch (error) {
console.error('Update profile error:', error)
message.error(error.response?.data?.detail || '更新失败')
} finally {
setLoading(false)
}
}
// 修改密码
const handleChangePassword = async (values) => {
setLoading(true)
try {
await changePassword({
old_password: values.old_password,
new_password: values.new_password,
})
Toast.success('修改成功', '密码已修改,请重新登录')
passwordForm.resetFields()
// 2秒后跳转到登录页
setTimeout(() => {
window.location.href = '/login'
}, 2000)
} catch (error) {
console.error('Change password error:', error)
message.error(error.response?.data?.detail || '修改密码失败')
} finally {
setLoading(false)
}
}
2026-01-13 13:21:47 +00:00
// 上传头像前的验证
const beforeAvatarUpload = (file) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
if (!isJpgOrPng) {
Toast.error('格式错误', '仅支持 JPG、PNG 格式的图片')
return false
}
const isLt1M = file.size / 1024 / 1024 < 1
if (!isLt1M) {
Toast.error('文件过大', '图片大小不能超过 1MB')
return false
}
return true
}
// 处理头像上传
const handleAvatarUpload = async (info) => {
const { file } = info
setLoading(true)
try {
const res = await uploadAvatar(file)
setUserInfo(res.data)
setUser(res.data) // 更新全局用户信息
Toast.success('上传成功', '头像已更新')
} catch (error) {
console.error('Upload avatar error:', error)
Toast.error('上传失败', error.response?.data?.detail || '头像上传失败')
} finally {
setLoading(false)
}
}
// 获取头像URL
const getAvatarUrl = () => {
if (!userInfo?.avatar) return null
// avatar 字段存储的是相对路径2/avatar/xxx.jpg
// 需要转换为 API 端点: /api/v1/auth/avatar/{user_id}/{filename}
const parts = userInfo.avatar.split('/')
if (parts.length >= 3) {
const userId = parts[0]
const filename = parts[2]
return `/api/v1/auth/avatar/${userId}/${filename}`
}
return null
}
2025-12-20 11:18:59 +00:00
const tabItems = [
{
key: 'profile',
label: (
<span>
<UserOutlined />
个人资料
</span>
),
children: (
<div className="profile-tab-content">
<div className="avatar-section">
2026-01-13 13:21:47 +00:00
<Avatar
size={100}
icon={<UserOutlined />}
src={getAvatarUrl()}
/>
<ImgCrop
rotationSlider
aspect={1}
quality={1}
modalTitle="裁剪头像"
modalOk="确定"
modalCancel="取消"
>
<Upload
showUploadList={false}
beforeUpload={beforeAvatarUpload}
customRequest={({ file }) => handleAvatarUpload({ file })}
>
<Button icon={<UploadOutlined />} style={{ marginTop: 16 }} loading={loading}>
更换头像
</Button>
</Upload>
</ImgCrop>
<p className="avatar-tip">支持 JPGPNG 格式文件小于 1MB</p>
2025-12-20 11:18:59 +00:00
</div>
<Form
form={profileForm}
layout="vertical"
onFinish={handleUpdateProfile}
className="profile-form"
>
<Form.Item label="用户名" name="username">
<Input disabled />
</Form.Item>
<Form.Item
label="昵称"
name="nickname"
rules={[{ max: 50, message: '昵称最多50个字符' }]}
>
<Input placeholder="请输入昵称" />
</Form.Item>
<Form.Item
label="邮箱"
name="email"
rules={[
{ type: 'email', message: '请输入有效的邮箱地址' },
]}
>
<Input placeholder="请输入邮箱" />
</Form.Item>
<Form.Item
label="手机号"
name="phone"
rules={[
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号' },
]}
>
<Input placeholder="请输入手机号" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>
保存修改
</Button>
</Form.Item>
</Form>
</div>
),
},
{
key: 'password',
label: (
<span>
<LockOutlined />
修改密码
</span>
),
children: (
<div className="password-tab-content">
<Form
form={passwordForm}
layout="vertical"
onFinish={handleChangePassword}
className="password-form"
>
<Form.Item
label="当前密码"
name="old_password"
rules={[{ required: true, message: '请输入当前密码' }]}
>
<Input.Password placeholder="请输入当前密码" />
</Form.Item>
<Form.Item
label="新密码"
name="new_password"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码至少6个字符' },
{ max: 50, message: '密码最多50个字符' },
]}
>
<Input.Password placeholder="请输入新密码至少6个字符" />
</Form.Item>
<Form.Item
label="确认新密码"
name="confirm_password"
dependencies={['new_password']}
rules={[
{ required: true, message: '请确认新密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('new_password') === value) {
return Promise.resolve()
}
return Promise.reject(new Error('两次输入的密码不一致'))
},
}),
]}
>
<Input.Password placeholder="请再次输入新密码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading}>
修改密码
</Button>
<Button
style={{ marginLeft: 8 }}
onClick={() => passwordForm.resetFields()}
>
重置
</Button>
</Form.Item>
<div className="password-tips">
<h4>密码安全建议</h4>
<ul>
<li>密码长度至少6个字符</li>
<li>包含字母数字和特殊字符的组合更安全</li>
<li>不要使用过于简单的密码"123456"</li>
<li>定期更换密码提高账户安全性</li>
</ul>
</div>
</Form>
</div>
),
},
]
return (
2025-12-29 12:53:50 +00:00
2025-12-20 11:18:59 +00:00
<div className="profile-page">
<Card className="profile-card">
<h2 className="profile-title">个人中心</h2>
<Tabs items={tabItems} defaultActiveKey="profile" />
</Card>
</div>
2025-12-29 12:53:50 +00:00
2025-12-20 11:18:59 +00:00
)
}
export default ProfilePage