备份:PWA配置前的完整版本
包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
This commit is contained in:
+189
-186
@@ -1,14 +1,15 @@
|
||||
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 { 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 useFormDraft from '../hooks/useFormDraft';
|
||||
import { useLanguageStore } from '../store/languageStore';
|
||||
import apiClient from '../utils/request';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
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);
|
||||
@@ -16,96 +17,79 @@ const ProfilePage: React.FC = () => {
|
||||
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);
|
||||
|
||||
// 表单草稿保护
|
||||
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()
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
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
|
||||
});
|
||||
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}`, {
|
||||
const response = await apiClient.put(`/users/${user?.id}/profile`, {
|
||||
...values,
|
||||
avatar: avatarUrl,
|
||||
passport: passportUrl,
|
||||
driverLicense: driverLicenseUrl
|
||||
driver_license: driverLicenseUrl
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
if (data.success) {
|
||||
message.success('个人信息已更新');
|
||||
// 更新authStore中的用户信息
|
||||
message.success(t('user.profileUpdated'));
|
||||
if (user) {
|
||||
const updatedUser = {
|
||||
...user,
|
||||
name: values.name,
|
||||
email: values.email,
|
||||
phone: values.phone,
|
||||
avatar: avatarUrl
|
||||
};
|
||||
setUser(updatedUser);
|
||||
setUser({ ...user, name: values.name, email: values.email, phone: values.phone, avatar: avatarUrl, passport: passportUrl, driverLicense: driverLicenseUrl });
|
||||
}
|
||||
} else {
|
||||
message.error(data.message || '更新失败');
|
||||
message.error(data.message || t('user.updateFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
message.error('更新失败,请重试');
|
||||
message.error(t('user.updateRetry'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -114,200 +98,219 @@ const ProfilePage: React.FC = () => {
|
||||
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('密码已更新');
|
||||
message.success(t('user.passwordUpdated'));
|
||||
setPasswordModalVisible(false);
|
||||
passwordForm.resetFields();
|
||||
} else {
|
||||
message.error(data.message || '密码更新失败');
|
||||
message.error(data.message || t('user.passwordUpdateFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
message.error('密码更新失败,请重试');
|
||||
message.error(t('user.passwordUpdateRetry'));
|
||||
}
|
||||
};
|
||||
|
||||
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 handleAvatarUpload = async (options: any) => {
|
||||
setUploading('avatar');
|
||||
const url = await uploadToCos(options.file);
|
||||
if (url) setAvatarUrl(url);
|
||||
setUploading(null);
|
||||
};
|
||||
|
||||
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 handlePassportUpload = async (options: any) => {
|
||||
setUploading('passport');
|
||||
const url = await uploadToCos(options.file);
|
||||
if (url) setPassportUrl(url);
|
||||
setUploading(null);
|
||||
};
|
||||
|
||||
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('驾照上传失败');
|
||||
}
|
||||
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: 24 }}>
|
||||
<div style={{ padding: isMobile ? 8 : 24 }}>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Title level={3}>个人信息</Title>
|
||||
<Paragraph type="secondary">管理个人账号信息</Paragraph>
|
||||
<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
|
||||
name="avatar"
|
||||
listType="picture-circle"
|
||||
customRequest={handleAvatarUpload}
|
||||
showUploadList={false}
|
||||
onChange={handleAvatarChange}
|
||||
maxCount={1}
|
||||
accept="image/*"
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<Avatar size={128} src={avatarUrl} />
|
||||
<Avatar size={128} src={avatarUrl} style={{ cursor: 'pointer' }} />
|
||||
) : (
|
||||
<Avatar size={128} icon={<UserOutlined />} />
|
||||
<Avatar size={128} icon={<UserOutlined />} style={{ cursor: 'pointer' }} />
|
||||
)}
|
||||
</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>
|
||||
<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} onValuesChange={handleFormChange}>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="姓名" name="name" rules={[{ required: true, message: '请输入姓名' }]}>
|
||||
<Input placeholder="请输入姓名" />
|
||||
<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={12}>
|
||||
<Form.Item label="手机号" name="phone" rules={[{ required: true, message: '请输入手机号' }]}>
|
||||
<Input placeholder="请输入手机号" prefix={<PhoneOutlined />} />
|
||||
<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={12}>
|
||||
<Form.Item label="邮箱" name="email" rules={[{ required: true, message: '请输入邮箱' }, { type: 'email', message: '请输入正确的邮箱地址' }]}>
|
||||
<Input placeholder="请输入邮箱" prefix={<MailOutlined />} />
|
||||
<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={12}>
|
||||
<Form.Item label="用户名" disabled>
|
||||
<Input value={user?.username} placeholder="用户名" />
|
||||
<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: 24 }}>
|
||||
<Title level={4}>证件上传</Title>
|
||||
<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={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 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={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 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 }}>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
保存修改
|
||||
</Button>
|
||||
<Button style={{ marginLeft: 16 }} onClick={() => setPasswordModalVisible(true)}>
|
||||
修改密码
|
||||
</Button>
|
||||
<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="修改密码"
|
||||
title={t('user.changePassword')}
|
||||
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 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="新密码" name="newPassword" rules={[{ required: true, message: '请输入新密码' }, { min: 6, message: '密码长度至少为6位' }]}>
|
||||
<Input.Password placeholder="请输入新密码" prefix={<LockOutlined />} />
|
||||
<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="确认新密码" name="confirmPassword" rules={[{ required: true, message: '请确认新密码' }, ({ getFieldValue }) => ({
|
||||
<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('两次输入的密码不一致'));
|
||||
if (!value || getFieldValue('newPassword') === value) return Promise.resolve();
|
||||
return Promise.reject(new Error(t('user.passwordMismatch')));
|
||||
}
|
||||
})]}>
|
||||
<Input.Password placeholder="请确认新密码" prefix={<LockOutlined />} />
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user