2026-05-15 12:02:24 +08:00
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
2026-04-19 19:15:01 +08:00
|
|
|
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';
|
2026-05-15 12:02:24 +08:00
|
|
|
import useFormDraft from '../hooks/useFormDraft';
|
2026-04-19 19:15:01 +08:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2026-05-15 12:02:24 +08:00
|
|
|
// 表单草稿保护
|
|
|
|
|
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()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
2026-04-19 19:15:01 +08:00
|
|
|
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>
|
|
|
|
|
|
2026-05-15 12:02:24 +08:00
|
|
|
<Form form={form} layout="vertical" onFinish={handleSubmit} onValuesChange={handleFormChange}>
|
2026-04-19 19:15:01 +08:00
|
|
|
<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;
|