Files
yunhaifinance/frontend/src/pages/ProfilePage.tsx
T
a273825743 706dcc24eb 备份:PWA配置前的完整版本
包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
2026-06-13 12:44:48 +08:00

318 lines
14 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import { Card, Typography, Form, Input, Button, Avatar, Space, Upload, message, Row, Col, Modal, Image, Popconfirm } from 'antd';
import { UserOutlined, LockOutlined, PhoneOutlined, MailOutlined, UploadOutlined, EyeOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons';
import { useAuthStore } from '../store/authStore';
import { useLanguageStore } from '../store/languageStore';
import apiClient from '../utils/request';
const { Title, Paragraph, Text } = Typography;
const ProfilePage: React.FC = () => {
const { user, setUser } = useAuthStore();
const { t, currentLanguage } = useLanguageStore();
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 [uploading, setUploading] = useState<string | null>(null);
const [previewVisible, setPreviewVisible] = useState(false);
const [previewTitle, setPreviewTitle] = useState('');
const [previewUrl, setPreviewUrl] = useState('');
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
useEffect(() => {
if (user) {
form.setFieldsValue({ name: user.name, phone: user.phone, email: user.email });
setAvatarUrl(user.avatar);
setPassportUrl(user.passport);
setDriverLicenseUrl(user.driverLicense);
}
const fetchProfile = async () => {
try {
const res = await apiClient.get('/users/me');
if (res.data.success) {
const d = res.data.data;
form.setFieldsValue({ name: d.name, phone: d.phone, email: d.email });
setAvatarUrl(d.avatar);
setPassportUrl(d.passport);
setDriverLicenseUrl(d.driverLicense);
if (user) {
setUser({ ...user, name: d.name, email: d.email, phone: d.phone, avatar: d.avatar, passport: d.passport, driverLicense: d.driverLicense });
}
}
} catch (e) {}
};
fetchProfile();
}, [user, form]);
const uploadToCos = async (file: File): Promise<string | null> => {
try {
const formData = new FormData();
formData.append('file', file);
const res = await apiClient.post('/upload/single', formData);
if (res.data.success) return res.data.data.url;
message.error(t('user.uploadFailed'));
return null;
} catch (e) {
message.error(t('user.uploadFailed'));
return null;
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setLoading(true);
const response = await apiClient.put(`/users/${user?.id}/profile`, {
...values,
avatar: avatarUrl,
passport: passportUrl,
driver_license: driverLicenseUrl
});
const data = response.data;
if (data.success) {
message.success(t('user.profileUpdated'));
if (user) {
setUser({ ...user, name: values.name, email: values.email, phone: values.phone, avatar: avatarUrl, passport: passportUrl, driverLicense: driverLicenseUrl });
}
} else {
message.error(data.message || t('user.updateFailed'));
}
} catch (error) {
message.error(t('user.updateRetry'));
} finally {
setLoading(false);
}
};
const handlePasswordSubmit = async () => {
try {
const values = await passwordForm.validateFields();
const response = await apiClient.put(`/users/${user?.id}/password`, {
currentPassword: values.currentPassword,
newPassword: values.newPassword
});
const data = response.data;
if (data.success) {
message.success(t('user.passwordUpdated'));
setPasswordModalVisible(false);
passwordForm.resetFields();
} else {
message.error(data.message || t('user.passwordUpdateFailed'));
}
} catch (error) {
message.error(t('user.passwordUpdateRetry'));
}
};
const handleAvatarUpload = async (options: any) => {
setUploading('avatar');
const url = await uploadToCos(options.file);
if (url) setAvatarUrl(url);
setUploading(null);
};
const handlePassportUpload = async (options: any) => {
setUploading('passport');
const url = await uploadToCos(options.file);
if (url) setPassportUrl(url);
setUploading(null);
};
const handleDriverLicenseUpload = async (options: any) => {
setUploading('driverLicense');
const url = await uploadToCos(options.file);
if (url) setDriverLicenseUrl(url);
setUploading(null);
};
const showPreview = (title: string, url: string) => {
setPreviewTitle(title);
setPreviewUrl(url);
setPreviewVisible(true);
};
const colSpan = isMobile ? 24 : 12;
return (
<div style={{ padding: isMobile ? 8 : 24 }}>
<div style={{ marginBottom: 24 }}>
<Title level={3}>{t('user.profile')}</Title>
<Paragraph type="secondary">{t('user.managingProfile')}</Paragraph>
</div>
<Card>
<div style={{ textAlign: 'center', marginBottom: 24 }}>
<Space direction="vertical" style={{ alignItems: 'center' }}>
<Upload
customRequest={handleAvatarUpload}
showUploadList={false}
accept="image/*"
>
{avatarUrl ? (
<Avatar size={128} src={avatarUrl} style={{ cursor: 'pointer' }} />
) : (
<Avatar size={128} icon={<UserOutlined />} style={{ cursor: 'pointer' }} />
)}
</Upload>
<Text>{t('user.clickToChangeAvatar')}</Text>
<Text strong>{user?.name || user?.username}</Text>
<Text type="secondary">{user?.role === 'admin' ? t('user.admin') : user?.role === 'manager' ? t('user.manager') : t('user.employee')}</Text>
</Space>
</div>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Row gutter={16}>
<Col span={colSpan}>
<Form.Item label={t('user.name')} name="name" rules={[{ required: true, message: t('common.inputPlaceholder') }]}>
<Input placeholder={t('common.inputPlaceholder')} />
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item label={t('user.phone')} name="phone" rules={[{ required: true, message: t('common.inputPlaceholder') }]}>
<Input placeholder={t('common.inputPlaceholder')} prefix={<PhoneOutlined />} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={colSpan}>
<Form.Item label={t('user.email')} name="email" rules={[{ required: true, message: t('common.inputPlaceholder') }, { type: 'email', message: 'Invalid email' }]}>
<Input placeholder="email@example.com" prefix={<MailOutlined />} />
</Form.Item>
</Col>
<Col span={colSpan}>
<Form.Item label={t('user.username')} disabled>
<Input value={user?.username} placeholder={t('user.username')} />
</Form.Item>
</Col>
</Row>
<div style={{ marginBottom: 16, marginTop: 8 }}>
<Title level={4}>{t('user.idDocument')}</Title>
<Text type="secondary">{t('user.idDocTip')}</Text>
</div>
<Row gutter={16}>
<Col span={colSpan}>
<Card
size="small"
title={t('user.passport')}
extra={passportUrl ? (
<Space size="small">
<Button size="small" icon={<EyeOutlined />} onClick={() => showPreview(t('user.passport'), passportUrl)}>{t('common.view')}</Button>
<Upload customRequest={handlePassportUpload} showUploadList={false} accept="image/*">
<Button size="small" icon={<ReloadOutlined />} loading={uploading === 'passport'}>{t('user.replace')}</Button>
</Upload>
<Popconfirm title={t('user.deletePassportConfirm')} okText={t('common.confirm')} cancelText={t('common.cancel')} onConfirm={() => setPassportUrl(undefined)}>
<Button size="small" danger icon={<DeleteOutlined />}>{t('common.delete')}</Button>
</Popconfirm>
</Space>
) : null}
style={{ marginBottom: 16 }}
>
{passportUrl ? (
<div style={{ cursor: 'pointer', textAlign: 'center' }} onClick={() => showPreview(t('user.passport'), passportUrl)}>
<img src={passportUrl} alt={t('user.passport')} style={{ maxWidth: '100%', maxHeight: 180, borderRadius: 4, objectFit: 'contain' }} />
</div>
) : (
<Upload customRequest={handlePassportUpload} showUploadList={false} accept="image/*">
<div style={{ textAlign: 'center', padding: 24, border: '1px dashed #d9d9d9', borderRadius: 4, cursor: 'pointer' }}>
<UploadOutlined style={{ fontSize: 32, color: '#1890ff' }} />
<div style={{ marginTop: 8 }}><Text type="secondary">{t('user.uploadPassport')}</Text></div>
</div>
</Upload>
)}
</Card>
</Col>
<Col span={colSpan}>
<Card
size="small"
title={t('user.driverLicense')}
extra={driverLicenseUrl ? (
<Space size="small">
<Button size="small" icon={<EyeOutlined />} onClick={() => showPreview(t('user.driverLicense'), driverLicenseUrl)}>{t('common.view')}</Button>
<Upload customRequest={handleDriverLicenseUpload} showUploadList={false} accept="image/*">
<Button size="small" icon={<ReloadOutlined />} loading={uploading === 'driverLicense'}>{t('user.replace')}</Button>
</Upload>
<Popconfirm title={t('user.deleteDriverLicenseConfirm')} okText={t('common.confirm')} cancelText={t('common.cancel')} onConfirm={() => setDriverLicenseUrl(undefined)}>
<Button size="small" danger icon={<DeleteOutlined />}>{t('common.delete')}</Button>
</Popconfirm>
</Space>
) : null}
style={{ marginBottom: 16 }}
>
{driverLicenseUrl ? (
<div style={{ cursor: 'pointer', textAlign: 'center' }} onClick={() => showPreview(t('user.driverLicense'), driverLicenseUrl)}>
<img src={driverLicenseUrl} alt={t('user.driverLicense')} style={{ maxWidth: '100%', maxHeight: 180, borderRadius: 4, objectFit: 'contain' }} />
</div>
) : (
<Upload customRequest={handleDriverLicenseUpload} showUploadList={false} accept="image/*">
<div style={{ textAlign: 'center', padding: 24, border: '1px dashed #d9d9d9', borderRadius: 4, cursor: 'pointer' }}>
<UploadOutlined style={{ fontSize: 32, color: '#1890ff' }} />
<div style={{ marginTop: 8 }}><Text type="secondary">{t('user.uploadDriverLicense')}</Text></div>
</div>
</Upload>
)}
</Card>
</Col>
</Row>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 24, gap: 8 }}>
<Button type="primary" htmlType="submit" loading={loading}>{t('user.saveProfile')}</Button>
<Button onClick={() => setPasswordModalVisible(true)}>{t('user.changePassword')}</Button>
</div>
</Form>
</Card>
<Modal
title={t('user.changePassword')}
open={passwordModalVisible}
onCancel={() => setPasswordModalVisible(false)}
onOk={handlePasswordSubmit}
width={400}
>
<Form form={passwordForm} layout="vertical">
<Form.Item label={t('user.currentPassword')} name="currentPassword" rules={[{ required: true, message: t('user.currentPasswordPlaceholder') }]}>
<Input.Password placeholder={t('user.currentPasswordPlaceholder')} prefix={<LockOutlined />} />
</Form.Item>
<Form.Item label={t('user.newPassword')} name="newPassword" rules={[{ required: true, message: t('user.newPasswordPlaceholder') }, { min: 6, message: t('user.passwordMinLen') }]}>
<Input.Password placeholder={t('user.newPasswordPlaceholder')} prefix={<LockOutlined />} />
</Form.Item>
<Form.Item label={t('user.confirmPassword')} name="confirmPassword" rules={[{ required: true, message: t('user.confirmPasswordPlaceholder') }, ({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) return Promise.resolve();
return Promise.reject(new Error(t('user.passwordMismatch')));
}
})]}>
<Input.Password placeholder={t('user.confirmPasswordPlaceholder')} prefix={<LockOutlined />} />
</Form.Item>
</Form>
</Modal>
<Modal
title={previewTitle}
open={previewVisible}
footer={null}
onCancel={() => setPreviewVisible(false)}
width={isMobile ? '95%' : 600}
centered
>
<div style={{ textAlign: 'center' }}>
<img src={previewUrl} alt={previewTitle} style={{ maxWidth: '100%', maxHeight: '70vh', objectFit: 'contain' }} />
</div>
</Modal>
</div>
);
};
export default ProfilePage;