2026-01-20 14:31:20 +08:00
|
|
|
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
2026-02-10 11:04:01 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Table,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
message,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Popconfirm,
|
|
|
|
|
|
Avatar,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
Badge,
|
|
|
|
|
|
} from 'antd';
|
|
|
|
|
|
import {
|
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
|
EditOutlined,
|
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
|
UserOutlined,
|
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
|
LockOutlined,
|
|
|
|
|
|
CameraOutlined,
|
|
|
|
|
|
CheckOutlined,
|
|
|
|
|
|
CloseOutlined,
|
|
|
|
|
|
} from '@ant-design/icons';
|
2025-12-25 10:48:35 +08:00
|
|
|
|
import { userAPI, roleAPI } from '../api';
|
|
|
|
|
|
|
|
|
|
|
|
const { Option } = Select;
|
|
|
|
|
|
|
|
|
|
|
|
const UserManagement = () => {
|
|
|
|
|
|
const [users, setUsers] = useState([]);
|
|
|
|
|
|
const [roles, setRoles] = useState([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
|
|
|
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|
|
|
|
|
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
|
|
|
|
|
|
const [avatarModalVisible, setAvatarModalVisible] = useState(false);
|
|
|
|
|
|
const [editingUser, setEditingUser] = useState(null);
|
|
|
|
|
|
const [passwordUser, setPasswordUser] = useState(null);
|
|
|
|
|
|
const [avatarUser, setAvatarUser] = useState(null);
|
|
|
|
|
|
const [uploadLoading, setUploadLoading] = useState(false);
|
2026-02-05 15:12:31 +08:00
|
|
|
|
const [activeTab, setActiveTab] = useState('all');
|
2025-12-25 10:48:35 +08:00
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
const [passwordForm] = Form.useForm();
|
|
|
|
|
|
const fileInputRef = useRef(null);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchUsers();
|
|
|
|
|
|
fetchRoles();
|
|
|
|
|
|
}, [pagination.current]);
|
|
|
|
|
|
|
2026-01-20 14:31:20 +08:00
|
|
|
|
const fetchUsers = useCallback(async () => {
|
2025-12-25 10:48:35 +08:00
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
2026-02-05 15:12:31 +08:00
|
|
|
|
const params = {
|
2025-12-25 10:48:35 +08:00
|
|
|
|
page: pagination.current,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
pageSize: pagination.pageSize,
|
2026-02-05 15:12:31 +08:00
|
|
|
|
};
|
|
|
|
|
|
if (activeTab !== 'all') {
|
|
|
|
|
|
params.status = activeTab;
|
|
|
|
|
|
}
|
|
|
|
|
|
const response = await userAPI.list(params);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
if (response.success) {
|
|
|
|
|
|
setUsers(response.data.users);
|
|
|
|
|
|
setPagination(prev => ({ ...prev, total: response.data.total }));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('获取用户列表失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
2026-02-05 15:12:31 +08:00
|
|
|
|
}, [pagination.current, pagination.pageSize, activeTab]);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
2026-01-20 14:31:20 +08:00
|
|
|
|
const fetchRoles = useCallback(async () => {
|
2025-12-25 10:48:35 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await roleAPI.all();
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
setRoles(response.data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error('获取角色列表失败: ' + (response.message || '未知错误'));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取角色列表失败:', error);
|
|
|
|
|
|
message.error('获取角色列表失败,请检查网络连接');
|
|
|
|
|
|
}
|
2026-01-20 14:31:20 +08:00
|
|
|
|
}, []);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
2026-01-20 14:31:20 +08:00
|
|
|
|
const handleAdd = useCallback(() => {
|
2025-12-25 10:48:35 +08:00
|
|
|
|
setEditingUser(null);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
setModalVisible(true);
|
2026-01-20 14:31:20 +08:00
|
|
|
|
}, []);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleEdit = useCallback(user => {
|
2025-12-25 10:48:35 +08:00
|
|
|
|
setEditingUser(user);
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
username: user.username,
|
|
|
|
|
|
email: user.email,
|
|
|
|
|
|
phone: user.phone,
|
|
|
|
|
|
realName: user.realName,
|
|
|
|
|
|
status: user.status,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
roleIds: user.roles?.map(r => r.roleId) || [],
|
2025-12-25 10:48:35 +08:00
|
|
|
|
});
|
|
|
|
|
|
setModalVisible(true);
|
2026-01-20 14:31:20 +08:00
|
|
|
|
}, []);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleResetPassword = useCallback(user => {
|
2025-12-25 10:48:35 +08:00
|
|
|
|
setPasswordUser(user);
|
|
|
|
|
|
passwordForm.resetFields();
|
|
|
|
|
|
setPasswordModalVisible(true);
|
2026-01-20 14:31:20 +08:00
|
|
|
|
}, []);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleAvatarClick = useCallback(user => {
|
2025-12-25 10:48:35 +08:00
|
|
|
|
setAvatarUser(user);
|
|
|
|
|
|
setAvatarModalVisible(true);
|
2026-01-20 14:31:20 +08:00
|
|
|
|
}, []);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleAvatarUpload = useCallback(
|
|
|
|
|
|
async e => {
|
|
|
|
|
|
const file = e.target.files[0];
|
|
|
|
|
|
if (!file) return;
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
if (!file.type.match(/image\/(jpeg|png|gif|webp)/)) {
|
|
|
|
|
|
message.error('只支持 JPG、PNG、GIF 和 WebP 格式的图片');
|
|
|
|
|
|
return;
|
2025-12-25 10:48:35 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
|
|
|
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
|
|
|
|
message.error('图片大小不能超过 5MB');
|
|
|
|
|
|
return;
|
2025-12-25 10:48:35 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
|
|
|
|
|
setUploadLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await userAPI.uploadAvatar(avatarUser.userId, file);
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
message.success('头像上传成功');
|
|
|
|
|
|
fetchUsers();
|
|
|
|
|
|
setAvatarModalVisible(false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(response.message || '上传失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('上传失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setUploadLoading(false);
|
|
|
|
|
|
if (fileInputRef.current) {
|
|
|
|
|
|
fileInputRef.current.value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[avatarUser, fetchUsers]
|
|
|
|
|
|
);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
2026-01-20 14:31:20 +08:00
|
|
|
|
const handleAvatarDelete = useCallback(async () => {
|
2025-12-25 10:48:35 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await userAPI.deleteAvatar(avatarUser.userId);
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
message.success('头像已删除');
|
|
|
|
|
|
fetchUsers();
|
|
|
|
|
|
setAvatarModalVisible(false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(response.message || '删除失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('删除失败');
|
|
|
|
|
|
}
|
2026-01-20 14:31:20 +08:00
|
|
|
|
}, [avatarUser, fetchUsers]);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleDelete = useCallback(
|
|
|
|
|
|
async userId => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await userAPI.delete(userId);
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
message.success('删除成功');
|
|
|
|
|
|
fetchUsers();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(response.message || '删除失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('删除失败');
|
2025-12-25 10:48:35 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
[fetchUsers]
|
|
|
|
|
|
);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleLockUnlock = useCallback(
|
|
|
|
|
|
async record => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const newStatus = record.status === 'locked' ? 'active' : 'locked';
|
|
|
|
|
|
const response = await userAPI.update(record.userId, {
|
|
|
|
|
|
status: newStatus,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
message.success(record.status === 'locked' ? '解锁成功' : '锁定成功');
|
|
|
|
|
|
fetchUsers();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(response.message || '操作失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('操作失败');
|
2026-01-21 12:20:51 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
[fetchUsers]
|
|
|
|
|
|
);
|
2026-01-21 12:20:51 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleSubmit = useCallback(
|
|
|
|
|
|
async values => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
let response;
|
|
|
|
|
|
if (editingUser) {
|
|
|
|
|
|
response = await userAPI.update(editingUser.userId, values);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
response = await userAPI.create(values);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
message.success(editingUser ? '更新成功' : '创建成功');
|
|
|
|
|
|
setModalVisible(false);
|
|
|
|
|
|
fetchUsers();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(response.message || '操作失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('操作失败');
|
2025-12-25 10:48:35 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
[editingUser, fetchUsers]
|
|
|
|
|
|
);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleResetPasswordSubmit = useCallback(
|
|
|
|
|
|
async values => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await userAPI.resetPassword(passwordUser.userId, values);
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
message.success('密码重置成功');
|
|
|
|
|
|
setPasswordModalVisible(false);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(response.message || '重置失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('重置失败');
|
2025-12-25 10:48:35 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
[passwordUser]
|
|
|
|
|
|
);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const getStatusColor = status => {
|
2025-12-25 10:48:35 +08:00
|
|
|
|
const colors = {
|
|
|
|
|
|
active: 'green',
|
|
|
|
|
|
inactive: 'red',
|
2026-02-05 15:12:31 +08:00
|
|
|
|
locked: 'orange',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
pending: 'blue',
|
2025-12-25 10:48:35 +08:00
|
|
|
|
};
|
|
|
|
|
|
return colors[status] || 'default';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const getStatusText = status => {
|
2025-12-25 10:48:35 +08:00
|
|
|
|
const texts = {
|
|
|
|
|
|
active: '正常',
|
|
|
|
|
|
inactive: '禁用',
|
2026-02-05 15:12:31 +08:00
|
|
|
|
locked: '锁定',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
pending: '待审核',
|
2025-12-25 10:48:35 +08:00
|
|
|
|
};
|
|
|
|
|
|
return texts[status] || status;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleApprove = useCallback(
|
|
|
|
|
|
async userId => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await userAPI.approve(userId);
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
message.success('审核通过');
|
|
|
|
|
|
fetchUsers();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(response.message || '审核失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('审核失败');
|
2026-02-05 15:12:31 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
[fetchUsers]
|
|
|
|
|
|
);
|
2026-02-05 15:12:31 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleReject = useCallback(
|
|
|
|
|
|
async userId => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await userAPI.reject(userId);
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
|
message.success('已拒绝该用户的注册申请');
|
|
|
|
|
|
fetchUsers();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(response.message || '操作失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('操作失败');
|
2026-02-05 15:12:31 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
[fetchUsers]
|
|
|
|
|
|
);
|
2026-02-05 15:12:31 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const getAvatarUrl = user => {
|
2025-12-25 10:48:35 +08:00
|
|
|
|
if (!user?.avatar) return null;
|
|
|
|
|
|
return user.avatar;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const tableColumns = useMemo(
|
|
|
|
|
|
() => [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '头像',
|
|
|
|
|
|
key: 'avatar',
|
|
|
|
|
|
width: 80,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Badge dot={!!record.avatar} color="green" offset={[-5, 35]}>
|
|
|
|
|
|
<Avatar
|
|
|
|
|
|
size={48}
|
|
|
|
|
|
icon={!record.avatar && <UserOutlined />}
|
|
|
|
|
|
src={getAvatarUrl(record)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
backgroundColor: record.avatar ? 'transparent' : '#1890ff',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
}}
|
|
|
|
|
|
onClick={() => handleAvatarClick(record)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '用户名',
|
|
|
|
|
|
key: 'username',
|
|
|
|
|
|
width: 150,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ fontWeight: 500 }}>{record.realName || record.username}</div>
|
|
|
|
|
|
<div style={{ fontSize: '12px', color: '#999' }}>@{record.username}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '邮箱',
|
|
|
|
|
|
dataIndex: 'email',
|
|
|
|
|
|
key: 'email',
|
|
|
|
|
|
width: 200,
|
|
|
|
|
|
render: email => email || '-',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '手机号',
|
|
|
|
|
|
dataIndex: 'phone',
|
|
|
|
|
|
key: 'phone',
|
|
|
|
|
|
width: 130,
|
|
|
|
|
|
render: phone => phone || '-',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '角色',
|
|
|
|
|
|
key: 'roles',
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space wrap>
|
|
|
|
|
|
{record.roles?.map(role => (
|
|
|
|
|
|
<Tag key={role.roleId} color={role.roleCode === 'admin' ? 'blue' : 'green'}>
|
|
|
|
|
|
{role.roleName}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
)) || '-'}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
dataIndex: 'status',
|
|
|
|
|
|
key: 'status',
|
|
|
|
|
|
render: status => <Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '最后登录',
|
|
|
|
|
|
key: 'lastLogin',
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<div style={{ fontSize: '12px' }}>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{record.lastLoginTime ? new Date(record.lastLoginTime).toLocaleString() : '从未登录'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ color: '#999' }}>{record.lastLoginIp || '-'}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space size="small">
|
|
|
|
|
|
{record.status === 'pending' ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Popconfirm
|
|
|
|
|
|
title="确定要通过该用户的注册申请吗?"
|
|
|
|
|
|
onConfirm={() => handleApprove(record.userId)}
|
|
|
|
|
|
okText="通过"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Tooltip title="通过">
|
|
|
|
|
|
<Button type="text" icon={<CheckOutlined />} style={{ color: '#52c41a' }} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
|
<Popconfirm
|
|
|
|
|
|
title="确定要拒绝该用户的注册申请吗?"
|
|
|
|
|
|
onConfirm={() => handleReject(record.userId)}
|
|
|
|
|
|
okText="拒绝"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Tooltip title="拒绝">
|
|
|
|
|
|
<Button type="text" icon={<CloseOutlined />} style={{ color: '#ff4d4f' }} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Tooltip title="编辑">
|
|
|
|
|
|
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip title="重置密码">
|
2026-02-05 15:12:31 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
2026-02-10 11:04:01 +08:00
|
|
|
|
icon={<LockOutlined />}
|
|
|
|
|
|
onClick={() => handleResetPassword(record)}
|
2026-02-05 15:12:31 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Popconfirm
|
|
|
|
|
|
title={
|
|
|
|
|
|
record.status === 'locked' ? '确定要解锁此用户吗?' : '确定要锁定此用户吗?'
|
|
|
|
|
|
}
|
|
|
|
|
|
onConfirm={() => handleLockUnlock(record)}
|
|
|
|
|
|
okText="确定"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Tooltip title={record.status === 'locked' ? '解锁' : '锁定'}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={record.status === 'locked' ? <ReloadOutlined /> : <LockOutlined />}
|
|
|
|
|
|
style={{ color: record.status === 'locked' ? '#52c41a' : '#faad14' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
|
<Popconfirm
|
|
|
|
|
|
title="确定要删除此用户吗?"
|
|
|
|
|
|
onConfirm={() => handleDelete(record.userId)}
|
|
|
|
|
|
okText="确定"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Tooltip title="删除">
|
|
|
|
|
|
<Button type="text" danger icon={<DeleteOutlined />} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
handleAvatarClick,
|
|
|
|
|
|
handleEdit,
|
|
|
|
|
|
handleResetPassword,
|
|
|
|
|
|
handleDelete,
|
|
|
|
|
|
handleLockUnlock,
|
|
|
|
|
|
handleApprove,
|
|
|
|
|
|
handleReject,
|
|
|
|
|
|
]
|
|
|
|
|
|
);
|
2025-12-25 10:48:35 +08:00
|
|
|
|
|
|
|
|
|
|
const pageHeaderStyle = {
|
|
|
|
|
|
marginBottom: '24px',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
flexWrap: 'wrap',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
gap: '16px',
|
2025-12-25 10:48:35 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const titleStyle = {
|
2025-12-29 11:15:55 +08:00
|
|
|
|
fontSize: '24px',
|
|
|
|
|
|
fontWeight: '700',
|
|
|
|
|
|
margin: 0,
|
|
|
|
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
|
|
|
WebkitBackgroundClip: 'text',
|
|
|
|
|
|
WebkitTextFillColor: 'transparent',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
backgroundClip: 'text',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const cardStyle = {
|
|
|
|
|
|
borderRadius: '16px',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.08)',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
overflow: 'hidden',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const cardHeadStyle = {
|
|
|
|
|
|
borderBottom: '1px solid #f0f0f0',
|
|
|
|
|
|
padding: '16px 24px',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
background: 'linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%)',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const primaryButtonStyle = {
|
|
|
|
|
|
height: '40px',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.35)',
|
|
|
|
|
|
fontWeight: '500',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
transition: 'all 0.3s ease',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const secondaryButtonStyle = {
|
|
|
|
|
|
height: '40px',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
border: '1px solid #e8e8e8',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
transition: 'all 0.3s ease',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const actionButtonStyle = {
|
|
|
|
|
|
height: '32px',
|
|
|
|
|
|
borderRadius: '6px',
|
|
|
|
|
|
border: '1px solid #e8e8e8',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
transition: 'all 0.3s ease',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const modalHeaderStyle = {
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '8px',
|
|
|
|
|
|
fontSize: '18px',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
fontWeight: '600',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const modalHeaderAccent = {
|
|
|
|
|
|
width: '4px',
|
|
|
|
|
|
height: '20px',
|
|
|
|
|
|
background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
borderRadius: '2px',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const avatarModalStyle = {
|
|
|
|
|
|
textAlign: 'center',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
padding: '20px 0',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const avatarWrapperStyle = {
|
|
|
|
|
|
marginBottom: '24px',
|
|
|
|
|
|
position: 'relative',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
display: 'inline-block',
|
2025-12-25 10:48:35 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<div style={{ padding: '8px' }}>
|
2025-12-25 10:48:35 +08:00
|
|
|
|
<div style={pageHeaderStyle}>
|
|
|
|
|
|
<h1 style={titleStyle}>用户管理</h1>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Space size="middle">
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button icon={<ReloadOutlined />} onClick={fetchUsers} style={secondaryButtonStyle}>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
刷新
|
|
|
|
|
|
</Button>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<PlusOutlined />}
|
2025-12-29 11:15:55 +08:00
|
|
|
|
onClick={handleAdd}
|
|
|
|
|
|
style={primaryButtonStyle}
|
|
|
|
|
|
>
|
2025-12-25 10:48:35 +08:00
|
|
|
|
添加用户
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-05 15:12:31 +08:00
|
|
|
|
<Card
|
|
|
|
|
|
style={cardStyle}
|
|
|
|
|
|
styles={{ header: cardHeadStyle, body: { padding: '20px 24px' } }}
|
|
|
|
|
|
tabList={[
|
|
|
|
|
|
{ key: 'all', tab: '全部用户' },
|
|
|
|
|
|
{ key: 'pending', tab: '待审核' },
|
|
|
|
|
|
{ key: 'active', tab: '正常' },
|
|
|
|
|
|
{ key: 'locked', tab: '锁定' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
{ key: 'inactive', tab: '禁用' },
|
2026-02-05 15:12:31 +08:00
|
|
|
|
]}
|
|
|
|
|
|
activeTabKey={activeTab}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onTabChange={key => {
|
2026-02-05 15:12:31 +08:00
|
|
|
|
setActiveTab(key);
|
|
|
|
|
|
setPagination(prev => ({ ...prev, current: 1 }));
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-12-25 10:48:35 +08:00
|
|
|
|
<Table
|
2026-01-20 14:31:20 +08:00
|
|
|
|
columns={tableColumns}
|
2025-12-25 10:48:35 +08:00
|
|
|
|
dataSource={users}
|
|
|
|
|
|
rowKey="userId"
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
pagination={{
|
|
|
|
|
|
...pagination,
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
showQuickJumper: true,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
showTotal: total => `共 ${total} 条记录`,
|
2025-12-25 10:48:35 +08:00
|
|
|
|
}}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onChange={newPagination => {
|
2025-12-25 10:48:35 +08:00
|
|
|
|
setPagination(prev => ({ ...prev, ...newPagination }));
|
|
|
|
|
|
}}
|
2025-12-29 11:15:55 +08:00
|
|
|
|
rowClassName={() => 'table-row'}
|
2025-12-25 10:48:35 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Modal
|
2025-12-29 11:15:55 +08:00
|
|
|
|
title={
|
|
|
|
|
|
<div style={modalHeaderStyle}>
|
|
|
|
|
|
<span style={modalHeaderAccent}></span>
|
|
|
|
|
|
{editingUser ? '编辑用户' : '添加用户'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2025-12-25 10:48:35 +08:00
|
|
|
|
open={modalVisible}
|
|
|
|
|
|
onCancel={() => setModalVisible(false)}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={500}
|
2025-12-29 13:33:26 +08:00
|
|
|
|
destroyOnHidden
|
2025-12-29 11:15:55 +08:00
|
|
|
|
styles={{
|
|
|
|
|
|
body: { padding: '24px' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
2025-12-29 11:15:55 +08:00
|
|
|
|
}}
|
|
|
|
|
|
style={{ borderRadius: '16px', overflow: 'hidden' }}
|
2025-12-25 10:48:35 +08:00
|
|
|
|
>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Form form={form} layout="vertical" onFinish={handleSubmit} style={{ marginTop: '20px' }}>
|
2025-12-25 10:48:35 +08:00
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="username"
|
|
|
|
|
|
label="用户名"
|
|
|
|
|
|
rules={[
|
|
|
|
|
|
{ required: true, message: '请输入用户名' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' },
|
2025-12-25 10:48:35 +08:00
|
|
|
|
]}
|
|
|
|
|
|
>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Input placeholder="请输入用户名" style={{ borderRadius: '8px' }} />
|
2025-12-25 10:48:35 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="realName"
|
|
|
|
|
|
label="真实姓名"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入真实姓名' }]}
|
|
|
|
|
|
>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Input placeholder="请输入真实姓名" style={{ borderRadius: '8px' }} />
|
2025-12-25 10:48:35 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="email"
|
|
|
|
|
|
label="邮箱"
|
|
|
|
|
|
rules={[
|
|
|
|
|
|
{ required: true, message: '请输入邮箱' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
{ type: 'email', message: '请输入有效的邮箱地址' },
|
2025-12-25 10:48:35 +08:00
|
|
|
|
]}
|
|
|
|
|
|
>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Input placeholder="请输入邮箱" style={{ borderRadius: '8px' }} />
|
2025-12-25 10:48:35 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item name="phone" label="手机号">
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Input placeholder="请输入手机号" style={{ borderRadius: '8px' }} />
|
2025-12-25 10:48:35 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="roleIds"
|
|
|
|
|
|
label="角色"
|
|
|
|
|
|
rules={[{ required: true, message: '请选择角色' }]}
|
|
|
|
|
|
>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Select mode="multiple" placeholder="请选择角色" style={{ borderRadius: '8px' }}>
|
2025-12-25 10:48:35 +08:00
|
|
|
|
{roles.map(role => (
|
|
|
|
|
|
<Option key={role.roleId} value={role.roleId}>
|
|
|
|
|
|
{role.roleName}
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
{!editingUser && (
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="password"
|
|
|
|
|
|
label="初始密码"
|
|
|
|
|
|
rules={[
|
|
|
|
|
|
{ required: true, message: '请输入初始密码' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
{ min: 6, message: '密码长度不能少于6个字符' },
|
2025-12-25 10:48:35 +08:00
|
|
|
|
]}
|
|
|
|
|
|
>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Input.Password placeholder="请输入初始密码" style={{ borderRadius: '8px' }} />
|
2025-12-25 10:48:35 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item name="status" label="状态">
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Select placeholder="请选择状态" style={{ borderRadius: '8px' }}>
|
2025-12-25 10:48:35 +08:00
|
|
|
|
<Option value="active">正常</Option>
|
|
|
|
|
|
<Option value="inactive">禁用</Option>
|
|
|
|
|
|
<Option value="locked">锁定</Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
{editingUser && (
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="newPassword"
|
|
|
|
|
|
label="新密码"
|
2026-02-10 11:04:01 +08:00
|
|
|
|
rules={[{ min: 6, message: '密码长度不能少于6个字符' }]}
|
2025-12-25 10:48:35 +08:00
|
|
|
|
>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Input.Password placeholder="留空则不修改密码" style={{ borderRadius: '8px' }} />
|
2025-12-25 10:48:35 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Form.Item style={{ marginBottom: 0, textAlign: 'right', marginTop: '24px' }}>
|
2025-12-25 10:48:35 +08:00
|
|
|
|
<Space>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button
|
2025-12-29 11:15:55 +08:00
|
|
|
|
onClick={() => setModalVisible(false)}
|
|
|
|
|
|
style={{ borderRadius: '8px', height: '40px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
htmlType="submit"
|
2025-12-29 11:15:55 +08:00
|
|
|
|
loading={loading}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
...primaryButtonStyle,
|
|
|
|
|
|
width: 'auto',
|
|
|
|
|
|
paddingLeft: '24px',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
paddingRight: '24px',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-12-25 10:48:35 +08:00
|
|
|
|
{editingUser ? '更新' : '创建'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
<Modal
|
2025-12-29 11:15:55 +08:00
|
|
|
|
title={
|
|
|
|
|
|
<div style={modalHeaderStyle}>
|
|
|
|
|
|
<span style={modalHeaderAccent}></span>
|
|
|
|
|
|
{`重置密码 - ${passwordUser?.username}`}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2025-12-25 10:48:35 +08:00
|
|
|
|
open={passwordModalVisible}
|
|
|
|
|
|
onCancel={() => setPasswordModalVisible(false)}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={400}
|
2025-12-29 13:33:26 +08:00
|
|
|
|
destroyOnHidden
|
2025-12-29 11:15:55 +08:00
|
|
|
|
styles={{
|
|
|
|
|
|
body: { padding: '24px' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
2025-12-29 11:15:55 +08:00
|
|
|
|
}}
|
|
|
|
|
|
style={{ borderRadius: '16px', overflow: 'hidden' }}
|
2025-12-25 10:48:35 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Form
|
|
|
|
|
|
form={passwordForm}
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
onFinish={handleResetPasswordSubmit}
|
|
|
|
|
|
style={{ marginTop: '20px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="newPassword"
|
|
|
|
|
|
label="新密码"
|
|
|
|
|
|
rules={[
|
|
|
|
|
|
{ required: true, message: '请输入新密码' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
{ min: 6, message: '密码长度不能少于6个字符' },
|
2025-12-25 10:48:35 +08:00
|
|
|
|
]}
|
|
|
|
|
|
>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Input.Password placeholder="请输入新密码" style={{ borderRadius: '8px' }} />
|
2025-12-25 10:48:35 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Form.Item style={{ marginBottom: 0, textAlign: 'right', marginTop: '24px' }}>
|
2025-12-25 10:48:35 +08:00
|
|
|
|
<Space>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button
|
2025-12-29 11:15:55 +08:00
|
|
|
|
onClick={() => setPasswordModalVisible(false)}
|
|
|
|
|
|
style={{ borderRadius: '8px', height: '40px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
htmlType="submit"
|
2025-12-29 11:15:55 +08:00
|
|
|
|
loading={loading}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
...primaryButtonStyle,
|
|
|
|
|
|
width: 'auto',
|
|
|
|
|
|
paddingLeft: '24px',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
paddingRight: '24px',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-12-25 10:48:35 +08:00
|
|
|
|
重置
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
<Modal
|
2025-12-29 11:15:55 +08:00
|
|
|
|
title={
|
|
|
|
|
|
<div style={modalHeaderStyle}>
|
|
|
|
|
|
<span style={modalHeaderAccent}></span>
|
|
|
|
|
|
{`设置头像 - ${avatarUser?.username}`}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2025-12-25 10:48:35 +08:00
|
|
|
|
open={avatarModalVisible}
|
|
|
|
|
|
onCancel={() => setAvatarModalVisible(false)}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={400}
|
2025-12-29 13:33:26 +08:00
|
|
|
|
destroyOnHidden
|
2025-12-29 11:15:55 +08:00
|
|
|
|
styles={{
|
|
|
|
|
|
body: { padding: '24px' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
2025-12-29 11:15:55 +08:00
|
|
|
|
}}
|
|
|
|
|
|
style={{ borderRadius: '16px', overflow: 'hidden' }}
|
2025-12-25 10:48:35 +08:00
|
|
|
|
>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<div style={avatarModalStyle}>
|
|
|
|
|
|
<div style={avatarWrapperStyle}>
|
2025-12-25 10:48:35 +08:00
|
|
|
|
<Badge dot={!!avatarUser?.avatar} color="green" offset={[-5, 35]}>
|
|
|
|
|
|
<Avatar
|
|
|
|
|
|
size={120}
|
|
|
|
|
|
icon={!avatarUser?.avatar && <UserOutlined />}
|
|
|
|
|
|
src={getAvatarUrl(avatarUser)}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
style={{
|
2025-12-25 10:48:35 +08:00
|
|
|
|
backgroundColor: avatarUser?.avatar ? 'transparent' : '#1890ff',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
border: '1px solid #f0f0f0',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
cursor: 'pointer',
|
2025-12-25 10:48:35 +08:00
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
accept="image/jpeg,image/png,image/gif,image/webp"
|
|
|
|
|
|
style={{ display: 'none' }}
|
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
|
onChange={handleAvatarUpload}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
2025-12-25 10:48:35 +08:00
|
|
|
|
icon={<CameraOutlined />}
|
|
|
|
|
|
onClick={() => fileInputRef.current?.click()}
|
|
|
|
|
|
loading={uploadLoading}
|
|
|
|
|
|
block
|
2025-12-29 11:15:55 +08:00
|
|
|
|
style={{
|
|
|
|
|
|
...primaryButtonStyle,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
height: '44px',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
}}
|
2025-12-25 10:48:35 +08:00
|
|
|
|
>
|
|
|
|
|
|
{avatarUser?.avatar ? '更换头像' : '上传头像'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
{avatarUser?.avatar && (
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
danger
|
2025-12-25 10:48:35 +08:00
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
|
onClick={handleAvatarDelete}
|
|
|
|
|
|
block
|
2025-12-29 11:15:55 +08:00
|
|
|
|
style={{ borderRadius: '8px', height: '44px' }}
|
2025-12-25 10:48:35 +08:00
|
|
|
|
>
|
|
|
|
|
|
删除头像
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<div style={{ marginTop: '16px', color: '#8c8c8c', fontSize: '12px' }}>
|
2025-12-25 10:48:35 +08:00
|
|
|
|
支持 JPG、PNG、GIF、WebP 格式,大小不超过 5MB
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default UserManagement;
|