Files
yunhaifinance/frontend/src/pages/ProfilePage.tsx
T

328 lines
12 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect, useCallback } from 'react';
import { Card, Typography, Form, Input, Button, Avatar, Space, Upload, message, Row, Col, Modal } from 'antd';
import { UserOutlined, LockOutlined, PhoneOutlined, MailOutlined, UploadOutlined } from '@ant-design/icons';
import { useAuthStore } from '../store/authStore';
import useFormDraft from '../hooks/useFormDraft';
const { Title, Paragraph } = Typography;
const ProfilePage: React.FC = () => {
const { user, setUser } = useAuthStore();
const [form] = Form.useForm();
const [passwordForm] = Form.useForm();
const [loading, setLoading] = useState(false);
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(user?.avatar);
const [passportUrl, setPassportUrl] = useState<string | undefined>(user?.passport);
const [driverLicenseUrl, setDriverLicenseUrl] = useState<string | undefined>(user?.driverLicense);
// 表单草稿保护
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
form,
storageKey: 'profile_edit',
onRestore: (data) => {
if (data.avatarUrl !== undefined) setAvatarUrl(data.avatarUrl)
if (data.passportUrl !== undefined) setPassportUrl(data.passportUrl)
if (data.driverLicenseUrl !== undefined) setDriverLicenseUrl(data.driverLicenseUrl)
},
})
const handleFormChange = useCallback(() => {
saveDraft({ avatarUrl, passportUrl, driverLicenseUrl })
}, [saveDraft, avatarUrl, passportUrl, driverLicenseUrl])
// 页面级 beforeunload 保护
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (form.isFieldsTouched()) {
e.preventDefault()
}
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
}, [form])
// 页面加载时检查草稿
useEffect(() => {
if (hasDraft()) {
Modal.confirm({
title: '发现未完成的草稿',
content: '检测到上次未保存的个人信息修改,是否恢复?',
okText: '恢复草稿',
cancelText: '放弃草稿',
onOk: () => {
restoreDraft()
},
onCancel: () => {
clearDraft()
},
})
}
}, [])
useEffect(() => {
if (user) {
form.setFieldsValue({
name: user.name,
phone: user.phone,
email: user.email
});
setAvatarUrl(user.avatar);
setPassportUrl(user.passport);
setDriverLicenseUrl(user.driverLicense);
}
}, [user, form]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setLoading(true);
// 调用API更新用户信息
const response = await fetch(`/api/users/${user?.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...values,
avatar: avatarUrl,
passport: passportUrl,
driverLicense: driverLicenseUrl
})
});
const data = await response.json();
if (data.success) {
message.success('个人信息已更新');
// 更新authStore中的用户信息
if (user) {
const updatedUser = {
...user,
name: values.name,
email: values.email,
phone: values.phone,
avatar: avatarUrl
};
setUser(updatedUser);
}
} else {
message.error(data.message || '更新失败');
}
} catch (error) {
console.error('提交失败:', error);
message.error('更新失败,请重试');
} finally {
setLoading(false);
}
};
const handlePasswordSubmit = async () => {
try {
const values = await passwordForm.validateFields();
// 调用API更新密码
const response = await fetch(`/api/users/${user?.id}/password`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
currentPassword: values.currentPassword,
newPassword: values.newPassword
})
});
const data = await response.json();
if (data.success) {
message.success('密码已更新');
setPasswordModalVisible(false);
passwordForm.resetFields();
} else {
message.error(data.message || '密码更新失败');
}
} catch (error) {
console.error('提交失败:', error);
message.error('密码更新失败,请重试');
}
};
const handleAvatarChange = (info: any) => {
if (info.file.status === 'done') {
setAvatarUrl(URL.createObjectURL(info.file.originFileObj));
message.success('头像上传成功');
} else if (info.file.status === 'error') {
message.error('头像上传失败');
}
};
const handlePassportChange = (info: any) => {
if (info.file.status === 'done') {
setPassportUrl(URL.createObjectURL(info.file.originFileObj));
message.success('护照上传成功');
} else if (info.file.status === 'error') {
message.error('护照上传失败');
}
};
const handleDriverLicenseChange = (info: any) => {
if (info.file.status === 'done') {
setDriverLicenseUrl(URL.createObjectURL(info.file.originFileObj));
message.success('驾照上传成功');
} else if (info.file.status === 'error') {
message.error('驾照上传失败');
}
};
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 24 }}>
<Title level={3}>个人信息</Title>
<Paragraph type="secondary">管理个人账号信息</Paragraph>
</div>
<Card>
<div style={{ textAlign: 'center', marginBottom: 24 }}>
<Space direction="vertical" style={{ alignItems: 'center' }}>
<Upload
name="avatar"
listType="picture-circle"
showUploadList={false}
onChange={handleAvatarChange}
maxCount={1}
>
{avatarUrl ? (
<Avatar size={128} src={avatarUrl} />
) : (
<Avatar size={128} icon={<UserOutlined />} />
)}
</Upload>
<Typography.Text>点击更换头像</Typography.Text>
<Typography.Text strong>{user?.name || user?.username}</Typography.Text>
<Typography.Text type="secondary">{user?.role === 'admin' ? '管理员' : user?.role === 'manager' ? '经理' : '普通用户'}</Typography.Text>
</Space>
</div>
<Form form={form} layout="vertical" onFinish={handleSubmit} onValuesChange={handleFormChange}>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="姓名" name="name" rules={[{ required: true, message: '请输入姓名' }]}>
<Input placeholder="请输入姓名" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="手机号" name="phone" rules={[{ required: true, message: '请输入手机号' }]}>
<Input placeholder="请输入手机号" prefix={<PhoneOutlined />} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="邮箱" name="email" rules={[{ required: true, message: '请输入邮箱' }, { type: 'email', message: '请输入正确的邮箱地址' }]}>
<Input placeholder="请输入邮箱" prefix={<MailOutlined />} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="用户名" disabled>
<Input value={user?.username} placeholder="用户名" />
</Form.Item>
</Col>
</Row>
<div style={{ marginBottom: 24 }}>
<Title level={4}>证件上传</Title>
</div>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="护照">
<Upload
name="passport"
listType="picture"
showUploadList={false}
onChange={handlePassportChange}
maxCount={1}
>
<Card
style={{ textAlign: 'center', padding: 24, border: '1px dashed #d9d9d9' }}
>
{passportUrl ? (
<img src={passportUrl} alt="护照" style={{ maxWidth: '100%', maxHeight: 200 }} />
) : (
<Space direction="vertical" style={{ alignItems: 'center' }}>
<UploadOutlined style={{ fontSize: 32, color: '#1890ff' }} />
<Typography.Text>点击上传护照</Typography.Text>
</Space>
)}
</Card>
</Upload>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="驾照">
<Upload
name="driverLicense"
listType="picture"
showUploadList={false}
onChange={handleDriverLicenseChange}
maxCount={1}
>
<Card
style={{ textAlign: 'center', padding: 24, border: '1px dashed #d9d9d9' }}
>
{driverLicenseUrl ? (
<img src={driverLicenseUrl} alt="驾照" style={{ maxWidth: '100%', maxHeight: 200 }} />
) : (
<Space direction="vertical" style={{ alignItems: 'center' }}>
<UploadOutlined style={{ fontSize: 32, color: '#1890ff' }} />
<Typography.Text>点击上传驾照</Typography.Text>
</Space>
)}
</Card>
</Upload>
</Form.Item>
</Col>
</Row>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 24 }}>
<Button type="primary" htmlType="submit" loading={loading}>
保存修改
</Button>
<Button style={{ marginLeft: 16 }} onClick={() => setPasswordModalVisible(true)}>
修改密码
</Button>
</div>
</Form>
</Card>
<Modal
title="修改密码"
open={passwordModalVisible}
onCancel={() => setPasswordModalVisible(false)}
onOk={handlePasswordSubmit}
width={400}
>
<Form form={passwordForm} layout="vertical">
<Form.Item label="当前密码" name="currentPassword" rules={[{ required: true, message: '请输入当前密码' }]}>
<Input.Password placeholder="请输入当前密码" prefix={<LockOutlined />} />
</Form.Item>
<Form.Item label="新密码" name="newPassword" rules={[{ required: true, message: '请输入新密码' }, { min: 6, message: '密码长度至少为6位' }]}>
<Input.Password placeholder="请输入新密码" prefix={<LockOutlined />} />
</Form.Item>
<Form.Item label="确认新密码" name="confirmPassword" rules={[{ required: true, message: '请确认新密码' }, ({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次输入的密码不一致'));
}
})]}>
<Input.Password placeholder="请确认新密码" prefix={<LockOutlined />} />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default ProfilePage;