import { PlusOutlined } from '@ant-design/icons';
|
import { Button, DatePicker, Form, Input, message, Modal, Radio, Select, Upload } from 'antd';
|
import dayjs from 'dayjs';
|
import React, { useEffect, useRef, useState } from 'react';
|
import { uploadUserImage } from './service';
|
|
type ClinicOption = {
|
id: number;
|
name: string;
|
};
|
|
type UserDetail = {
|
id: number;
|
nickname: string;
|
phone: string;
|
avatar?: string;
|
status: number;
|
memberExpireAt?: string | null;
|
clinic?: {
|
id: number;
|
name: string;
|
} | null;
|
};
|
|
type UserEditModalProps = {
|
open: boolean;
|
submitting: boolean;
|
form: any;
|
clinics: ClinicOption[];
|
detail?: UserDetail | null;
|
userType?: number;
|
onCancel: () => void;
|
onSubmit: () => void;
|
};
|
|
type PasswordModalProps = {
|
open: boolean;
|
submitting: boolean;
|
username?: string;
|
form: any;
|
onCancel: () => void;
|
onSubmit: () => void;
|
};
|
|
const passwordRules = [
|
{ required: true, message: '请输入新密码' },
|
{
|
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z\d]).{12,64}$/,
|
message: '请输入至少12位含大小写字母、数字和特殊字符的密码',
|
},
|
];
|
|
export const UserEditModal: React.FC<UserEditModalProps> = ({
|
open,
|
submitting,
|
form,
|
clinics,
|
detail,
|
userType,
|
onCancel,
|
onSubmit,
|
}) => {
|
const avatarUrl = Form.useWatch('avatar', form);
|
const [avatarPreview, setAvatarPreview] = useState<string>('');
|
const blobPreviewRef = useRef<string>('');
|
|
const clearBlobPreview = () => {
|
if (blobPreviewRef.current) {
|
URL.revokeObjectURL(blobPreviewRef.current);
|
blobPreviewRef.current = '';
|
}
|
};
|
|
useEffect(() => {
|
if (!open) {
|
return;
|
}
|
if (!detail) {
|
form.resetFields();
|
clearBlobPreview();
|
setAvatarPreview('');
|
return;
|
}
|
fillEditForm(form, detail);
|
clearBlobPreview();
|
setAvatarPreview(detail.avatar || '');
|
}, [detail, form, open]);
|
|
useEffect(() => {
|
if (!open) {
|
clearBlobPreview();
|
setAvatarPreview('');
|
}
|
}, [open]);
|
|
useEffect(() => () => clearBlobPreview(), []);
|
|
return (
|
<Modal
|
centered
|
destroyOnClose
|
open={open}
|
title="编辑用户"
|
width={520}
|
onCancel={onCancel}
|
footer={null}
|
className="member-user-edit-modal"
|
>
|
<Form form={form} layout="vertical" className="member-user-edit-form">
|
<Form.Item name="avatar" hidden>
|
<Input />
|
</Form.Item>
|
<Form.Item label="头像" className="member-user-edit-form__avatar">
|
<Upload
|
accept="image/*"
|
showUploadList={false}
|
customRequest={async (options) => {
|
try {
|
const file = options.file as File;
|
const localPreview = URL.createObjectURL(file);
|
clearBlobPreview();
|
blobPreviewRef.current = localPreview;
|
setAvatarPreview(localPreview);
|
const res = await uploadUserImage(file);
|
const url = res.data?.url;
|
if (!url) {
|
clearBlobPreview();
|
throw new Error('上传失败');
|
}
|
form.setFieldValue('avatar', url);
|
options.onSuccess?.(res);
|
} catch (error) {
|
clearBlobPreview();
|
setAvatarPreview(avatarUrl || detail?.avatar || '');
|
message.error('头像上传失败');
|
options.onError?.(error as Error);
|
}
|
}}
|
>
|
<div className="member-user-edit-form__avatar-box">
|
{avatarPreview ? (
|
<img key={avatarPreview} src={avatarPreview} alt="头像" />
|
) : (
|
<PlusOutlined />
|
)}
|
</div>
|
</Upload>
|
</Form.Item>
|
<Form.Item
|
label="用户名称"
|
name="nickname"
|
rules={[{ required: true, message: '请输入用户名称' }]}
|
>
|
<Input placeholder="请输入" maxLength={64} />
|
</Form.Item>
|
<Form.Item
|
label="登录账号"
|
name="phone"
|
rules={[
|
{ required: true, message: '请输入登录账号' },
|
{ pattern: /^1\d{10}$/, message: '请输入正确的登录账号' },
|
]}
|
>
|
<Input placeholder="请输入" maxLength={11} />
|
</Form.Item>
|
<Form.Item
|
label="状态"
|
name="status"
|
rules={[{ required: true, message: '请选择状态' }]}
|
>
|
<Radio.Group>
|
<Radio value={1}>启用</Radio>
|
<Radio value={0}>禁用</Radio>
|
</Radio.Group>
|
</Form.Item>
|
<Form.Item label="会员有效期" name="memberExpireAt">
|
<DatePicker placeholder="年 / 月 / 日" className="member-user-edit-form__full" />
|
</Form.Item>
|
{userType === 1 ? null : (
|
<Form.Item label="绑定诊所" name="clinicId">
|
<Select
|
allowClear
|
showSearch
|
optionFilterProp="label"
|
placeholder="请选择"
|
options={clinics.map((item) => ({ label: item.name, value: item.id }))}
|
/>
|
</Form.Item>
|
)}
|
<div className="member-user-edit-form__footer">
|
<Button onClick={onCancel}>取消</Button>
|
<Button type="primary" loading={submitting} onClick={onSubmit}>
|
保存
|
</Button>
|
</div>
|
</Form>
|
</Modal>
|
);
|
};
|
|
export const ResetPasswordModal: React.FC<PasswordModalProps> = ({
|
open,
|
submitting,
|
username,
|
form,
|
onCancel,
|
onSubmit,
|
}) => {
|
return (
|
<Modal
|
centered
|
destroyOnClose
|
open={open}
|
title="重置密码"
|
width={640}
|
onCancel={onCancel}
|
footer={null}
|
className="member-user-password-modal"
|
>
|
<Form form={form} layout="vertical" className="member-user-password-form">
|
<Form.Item label="用户">
|
<Input value={username} disabled />
|
</Form.Item>
|
<Form.Item label="新密码" name="newPassword" rules={passwordRules}>
|
<Input.Password placeholder="请输入至少12位含大小写字母、数字和特殊字符的密码" maxLength={64} />
|
</Form.Item>
|
<Form.Item
|
label="确认密码"
|
name="confirmPassword"
|
dependencies={['newPassword']}
|
rules={[
|
{ required: true, message: '请输入确认密码' },
|
({ getFieldValue }) => ({
|
validator(_, value) {
|
if (!value || getFieldValue('newPassword') === value) {
|
return Promise.resolve();
|
}
|
return Promise.reject(new Error('两次密码输入不一致'));
|
},
|
}),
|
]}
|
>
|
<Input.Password placeholder="请输入至少12位含大小写字母、数字和特殊字符的密码" maxLength={64} />
|
</Form.Item>
|
<div className="member-user-password-form__footer">
|
<Button onClick={onCancel}>取消</Button>
|
<Button type="primary" loading={submitting} onClick={onSubmit}>
|
保存
|
</Button>
|
</div>
|
</Form>
|
</Modal>
|
);
|
};
|
|
export const fillEditForm = (form: any, detail: UserDetail | null) => {
|
if (!detail) {
|
form.resetFields();
|
return;
|
}
|
form.setFieldsValue({
|
nickname: detail.nickname,
|
phone: detail.phone,
|
avatar: detail.avatar,
|
status: detail.status,
|
clinicId: detail.clinic?.id,
|
memberExpireAt: detail.memberExpireAt ? dayjs(detail.memberExpireAt) : undefined,
|
});
|
};
|