517 lines
17 KiB
JavaScript
517 lines
17 KiB
JavaScript
import React, { useEffect, useMemo, useState } from 'react';
|
||
import { Avatar, Button, Form, Input, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Typography, message } from 'antd';
|
||
import { AppstoreOutlined, EditOutlined, LockOutlined, PlusOutlined, ReloadOutlined, SyncOutlined, DeleteOutlined, SafetyCertificateOutlined, UserOutlined } from '@ant-design/icons';
|
||
import dayjs from 'dayjs';
|
||
import { SycmCard } from '../../components/Common';
|
||
import {
|
||
createUser,
|
||
deleteUser,
|
||
getDepartments,
|
||
getUserPagePermissions,
|
||
getUsers,
|
||
resetPassword,
|
||
syncDingTalkUsers,
|
||
updateUser,
|
||
updateUserAdminRole,
|
||
updateUserPagePermissions,
|
||
} from '../../api/admin';
|
||
|
||
const { Text } = Typography;
|
||
const { Search } = Input;
|
||
|
||
const ROLE_LABELS = {
|
||
admin: '管理员',
|
||
analyst: '分析师',
|
||
user: '用户',
|
||
guest: '访客',
|
||
};
|
||
|
||
const SOURCE_META = {
|
||
local: { color: 'default', text: '本地账号' },
|
||
dingtalk: { color: 'blue', text: '钉钉' },
|
||
google: { color: 'red', text: 'Google' },
|
||
};
|
||
|
||
const UserManagement = () => {
|
||
const [users, setUsers] = useState([]);
|
||
const [departments, setDepartments] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [saving, setSaving] = useState(false);
|
||
const [syncing, setSyncing] = useState(false);
|
||
const [switchingAdminId, setSwitchingAdminId] = useState(null);
|
||
const [modalVisible, setModalVisible] = useState(false);
|
||
const [resetVisible, setResetVisible] = useState(false);
|
||
const [permissionVisible, setPermissionVisible] = useState(false);
|
||
const [editingUser, setEditingUser] = useState(null);
|
||
const [resetUser, setResetUser] = useState(null);
|
||
const [permissionUser, setPermissionUser] = useState(null);
|
||
const [pagePermissions, setPagePermissions] = useState([]);
|
||
const [permissionSaving, setPermissionSaving] = useState(false);
|
||
const [keyword, setKeyword] = useState('');
|
||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
||
const [form] = Form.useForm();
|
||
const [resetForm] = Form.useForm();
|
||
|
||
const departmentOptions = useMemo(
|
||
() =>
|
||
departments
|
||
.filter((item) => item.status === 'active')
|
||
.map((item) => ({
|
||
label: item.name,
|
||
value: item.id,
|
||
})),
|
||
[departments]
|
||
);
|
||
|
||
const fetchDepartments = async () => {
|
||
try {
|
||
const data = await getDepartments();
|
||
setDepartments(Array.isArray(data) ? data : []);
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '获取部门列表失败');
|
||
}
|
||
};
|
||
|
||
const fetchUsers = async (page = pagination.current, pageSize = pagination.pageSize, nextKeyword = keyword) => {
|
||
try {
|
||
setLoading(true);
|
||
const res = await getUsers({
|
||
page,
|
||
size: pageSize,
|
||
keyword: nextKeyword,
|
||
});
|
||
setUsers(res.data || []);
|
||
setPagination({
|
||
current: res.page || page,
|
||
pageSize: res.size || pageSize,
|
||
total: res.total || 0,
|
||
});
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '获取用户列表失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
fetchDepartments();
|
||
fetchUsers(1, 10, '');
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, []);
|
||
|
||
const openCreateModal = () => {
|
||
setEditingUser(null);
|
||
form.resetFields();
|
||
form.setFieldsValue({
|
||
role: 'user',
|
||
status: 'active',
|
||
});
|
||
setModalVisible(true);
|
||
};
|
||
|
||
const openEditModal = (record) => {
|
||
setEditingUser(record);
|
||
form.setFieldsValue({
|
||
username: record.username,
|
||
name: record.name,
|
||
email: record.email,
|
||
mobile: record.mobile,
|
||
jobTitle: record.jobTitle,
|
||
role: record.role,
|
||
departmentId: record.department_id || undefined,
|
||
status: record.status,
|
||
});
|
||
setModalVisible(true);
|
||
};
|
||
|
||
const handleSave = async () => {
|
||
try {
|
||
const values = await form.validateFields();
|
||
setSaving(true);
|
||
if (editingUser) {
|
||
await updateUser(editingUser.id, values);
|
||
message.success('用户信息已更新');
|
||
} else {
|
||
await createUser(values);
|
||
message.success('用户已创建');
|
||
}
|
||
setModalVisible(false);
|
||
fetchUsers();
|
||
} catch (error) {
|
||
if (!error?.errorFields) {
|
||
message.error(error.response?.data?.error || '保存用户失败');
|
||
}
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleDelete = async (id) => {
|
||
try {
|
||
await deleteUser(id);
|
||
message.success('用户已删除');
|
||
fetchUsers();
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '删除用户失败');
|
||
}
|
||
};
|
||
|
||
const handleToggleAdminRole = async (record) => {
|
||
const nextIsAdmin = record.role !== 'admin';
|
||
|
||
try {
|
||
setSwitchingAdminId(record.id);
|
||
const res = await updateUserAdminRole(record.id, { isAdmin: nextIsAdmin });
|
||
message.success(res.message || (nextIsAdmin ? '已授予管理员权限' : '已取消管理员权限'));
|
||
fetchUsers(pagination.current, pagination.pageSize, keyword);
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '更新管理员权限失败');
|
||
} finally {
|
||
setSwitchingAdminId(null);
|
||
}
|
||
};
|
||
|
||
const openPagePermissionModal = async (record) => {
|
||
try {
|
||
setPermissionUser(record);
|
||
const data = await getUserPagePermissions(record.id);
|
||
setPagePermissions(Array.isArray(data.permissions) ? data.permissions : []);
|
||
setPermissionVisible(true);
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '获取用户可见页面失败');
|
||
}
|
||
};
|
||
|
||
const handlePagePermissionToggle = (pageKey, userAllowed) => {
|
||
setPagePermissions((prev) =>
|
||
prev.map((item) => (item.pageKey === pageKey ? { ...item, userAllowed, effectiveAllowed: Boolean(item.baseAllowed) || userAllowed } : item))
|
||
);
|
||
};
|
||
|
||
const handleSavePagePermissions = async () => {
|
||
if (!permissionUser) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setPermissionSaving(true);
|
||
const res = await updateUserPagePermissions(permissionUser.id, {
|
||
permissions: pagePermissions.map((item) => ({
|
||
pageKey: item.pageKey,
|
||
allowed: Boolean(item.userAllowed),
|
||
})),
|
||
});
|
||
setPagePermissions(Array.isArray(res.permissions) ? res.permissions : []);
|
||
message.success(res.message || '用户可见页面已更新');
|
||
setPermissionVisible(false);
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '更新用户可见页面失败');
|
||
} finally {
|
||
setPermissionSaving(false);
|
||
}
|
||
};
|
||
|
||
const openResetModal = (record) => {
|
||
setResetUser(record);
|
||
resetForm.resetFields();
|
||
setResetVisible(true);
|
||
};
|
||
|
||
const handleResetPassword = async () => {
|
||
try {
|
||
const values = await resetForm.validateFields();
|
||
setSaving(true);
|
||
await resetPassword(resetUser.id, { newPassword: values.newPassword });
|
||
message.success('密码已重置');
|
||
setResetVisible(false);
|
||
} catch (error) {
|
||
if (!error?.errorFields) {
|
||
message.error(error.response?.data?.error || '重置密码失败');
|
||
}
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleSync = async () => {
|
||
try {
|
||
setSyncing(true);
|
||
const res = await syncDingTalkUsers();
|
||
message.success(`同步完成,共处理 ${res.total || 0} 位钉钉用户`);
|
||
await Promise.all([fetchUsers(1, pagination.pageSize, keyword), fetchDepartments()]);
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '同步钉钉用户失败');
|
||
} finally {
|
||
setSyncing(false);
|
||
}
|
||
};
|
||
|
||
const columns = [
|
||
{
|
||
title: '用户',
|
||
key: 'user',
|
||
width: 280,
|
||
render: (_, record) => (
|
||
<Space align="start">
|
||
<Avatar src={record.avatarUrl} icon={<UserOutlined />} size={42} />
|
||
<div>
|
||
<div style={{ fontWeight: 600, color: 'var(--sycm-text-strong)' }}>{record.name || record.username}</div>
|
||
<div style={{ color: 'var(--sycm-text-tertiary)', fontSize: 13 }}>{record.username}</div>
|
||
<div style={{ color: 'var(--sycm-text-tertiary)', fontSize: 13 }}>{record.email}</div>
|
||
</div>
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '来源',
|
||
dataIndex: 'authSource',
|
||
width: 110,
|
||
render: (value) => {
|
||
const meta = SOURCE_META[value] || { color: 'default', text: value || '未知' };
|
||
return <Tag color={meta.color}>{meta.text}</Tag>;
|
||
},
|
||
},
|
||
{
|
||
title: '部门信息',
|
||
key: 'department',
|
||
width: 220,
|
||
render: (_, record) => (
|
||
<div>
|
||
<div>{record.departmentNames || record.departmentName || '-'}</div>
|
||
{record.jobTitle && <Text type="secondary">{record.jobTitle}</Text>}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '角色 / 状态',
|
||
key: 'roleStatus',
|
||
width: 160,
|
||
render: (_, record) => (
|
||
<Space direction="vertical" size={4}>
|
||
<Tag color={record.role === 'admin' ? 'gold' : 'default'}>{ROLE_LABELS[record.role] || record.role}</Tag>
|
||
<Tag color={record.status === 'active' ? 'success' : 'default'}>{record.status === 'active' ? '启用' : '停用'}</Tag>
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '联系信息',
|
||
key: 'contact',
|
||
width: 180,
|
||
render: (_, record) => (
|
||
<div>
|
||
<div>{record.mobile || '-'}</div>
|
||
{(record.dingtalkUserId || record.googleSub) && (
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
{record.dingtalkUserId ? `钉钉ID: ${record.dingtalkUserId}` : `Google: ${record.googleSub}`}
|
||
</Text>
|
||
)}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '最后登录',
|
||
dataIndex: 'lastLoginAt',
|
||
width: 170,
|
||
render: (value) => (value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '-'),
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
width: 320,
|
||
fixed: 'right',
|
||
render: (_, record) => (
|
||
<Space wrap>
|
||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openEditModal(record)}>
|
||
编辑
|
||
</Button>
|
||
<Button type="link" size="small" icon={<AppstoreOutlined />} onClick={() => openPagePermissionModal(record)}>
|
||
页面权限
|
||
</Button>
|
||
<Popconfirm
|
||
title={record.role === 'admin' ? '确认取消该用户的管理员权限?' : '确认授予该用户管理员权限?'}
|
||
description={record.role === 'admin' ? '取消后该用户将变为普通用户。' : '授予后该用户将拥有系统管理权限。'}
|
||
onConfirm={() => handleToggleAdminRole(record)}
|
||
>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
icon={<SafetyCertificateOutlined />}
|
||
loading={switchingAdminId === record.id}
|
||
>
|
||
{record.role === 'admin' ? '取消管理员' : '设为管理员'}
|
||
</Button>
|
||
</Popconfirm>
|
||
<Button type="link" size="small" icon={<LockOutlined />} onClick={() => openResetModal(record)}>
|
||
重置密码
|
||
</Button>
|
||
<Popconfirm title="确定删除该用户吗?" onConfirm={() => handleDelete(record.id)}>
|
||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||
删除
|
||
</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div className="settings-tab-page">
|
||
<SycmCard bodyStyle={{ padding: 16 }}>
|
||
<div style={{ display: 'flex', gap: 12, justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap' }}>
|
||
<Search
|
||
placeholder="搜索账号、姓名、邮箱、手机号、部门"
|
||
allowClear
|
||
onSearch={(value) => {
|
||
setKeyword(value);
|
||
fetchUsers(1, pagination.pageSize, value);
|
||
}}
|
||
style={{ width: 320 }}
|
||
/>
|
||
<Space wrap>
|
||
<Button icon={<ReloadOutlined />} onClick={() => fetchUsers()}>
|
||
刷新
|
||
</Button>
|
||
<Button icon={<SyncOutlined />} loading={syncing} onClick={handleSync}>
|
||
同步钉钉用户
|
||
</Button>
|
||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
|
||
新增用户
|
||
</Button>
|
||
</Space>
|
||
</div>
|
||
|
||
<Table
|
||
rowKey="id"
|
||
columns={columns}
|
||
dataSource={users}
|
||
loading={loading}
|
||
scroll={{ x: 1420 }}
|
||
pagination={{
|
||
current: pagination.current,
|
||
pageSize: pagination.pageSize,
|
||
total: pagination.total,
|
||
onChange: (page, pageSize) => fetchUsers(page, pageSize),
|
||
}}
|
||
/>
|
||
</SycmCard>
|
||
|
||
<Modal
|
||
title={editingUser ? '编辑用户' : '新增用户'}
|
||
open={modalVisible}
|
||
onCancel={() => setModalVisible(false)}
|
||
onOk={handleSave}
|
||
confirmLoading={saving}
|
||
width={560}
|
||
>
|
||
<Form form={form} layout="vertical" style={{ marginTop: 20 }}>
|
||
<Form.Item label="用户名" name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
||
<Input disabled={Boolean(editingUser)} />
|
||
</Form.Item>
|
||
{!editingUser && (
|
||
<Form.Item label="初始密码" name="password" rules={[{ required: true, message: '请输入初始密码' }]}>
|
||
<Input.Password />
|
||
</Form.Item>
|
||
)}
|
||
<Form.Item label="姓名" name="name" rules={[{ required: true, message: '请输入姓名' }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item label="邮箱" name="email" rules={[{ required: true, message: '请输入邮箱' }, { type: 'email', message: '邮箱格式不正确' }]}>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item label="手机号" name="mobile">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item label="职位" name="jobTitle">
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item label="所属部门" name="departmentId">
|
||
<Select allowClear options={departmentOptions} placeholder="请选择部门" />
|
||
</Form.Item>
|
||
<Space style={{ width: '100%' }} size={16}>
|
||
<Form.Item label="角色" name="role" style={{ flex: 1 }} rules={[{ required: true, message: '请选择角色' }]}>
|
||
<Select
|
||
options={Object.entries(ROLE_LABELS).map(([value, label]) => ({
|
||
value,
|
||
label,
|
||
}))}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item label="状态" name="status" style={{ flex: 1 }} rules={[{ required: true, message: '请选择状态' }]}>
|
||
<Select
|
||
options={[
|
||
{ value: 'active', label: '启用' },
|
||
{ value: 'inactive', label: '停用' },
|
||
]}
|
||
/>
|
||
</Form.Item>
|
||
</Space>
|
||
</Form>
|
||
</Modal>
|
||
|
||
<Modal
|
||
title={permissionUser ? `页面权限 - ${permissionUser.name || permissionUser.username}` : '页面权限'}
|
||
open={permissionVisible}
|
||
onCancel={() => setPermissionVisible(false)}
|
||
onOk={handleSavePagePermissions}
|
||
confirmLoading={permissionSaving}
|
||
width={680}
|
||
>
|
||
<div style={{ marginTop: 12 }}>
|
||
<Text type="secondary">
|
||
这里配置用户级额外可见页面。即使所属部门关闭了某个页面,只要在这里单独开通,该用户仍会在侧边栏看到该页面。
|
||
</Text>
|
||
<div style={{ marginTop: 20, display: 'grid', gap: 12 }}>
|
||
{pagePermissions.map((item) => (
|
||
<div
|
||
key={item.pageKey}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
gap: 16,
|
||
padding: '12px 14px',
|
||
borderRadius: 8,
|
||
background: 'var(--sycm-bg-subtle)',
|
||
}}
|
||
>
|
||
<div>
|
||
<Space size={8} wrap>
|
||
<span style={{ fontWeight: 600 }}>{item.pageName}</span>
|
||
{item.baseAllowed ? <Tag color="green">默认可见</Tag> : <Tag>默认不可见</Tag>}
|
||
{item.userAllowed ? <Tag color="blue">单独开通</Tag> : null}
|
||
</Space>
|
||
<div>
|
||
<Text type="secondary">{item.path}</Text>
|
||
</div>
|
||
</div>
|
||
<Switch
|
||
checked={Boolean(item.userAllowed)}
|
||
checkedChildren="开通"
|
||
unCheckedChildren="未开通"
|
||
onChange={(value) => handlePagePermissionToggle(item.pageKey, value)}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
|
||
<Modal
|
||
title={resetUser ? `重置密码 - ${resetUser.name || resetUser.username}` : '重置密码'}
|
||
open={resetVisible}
|
||
onCancel={() => setResetVisible(false)}
|
||
onOk={handleResetPassword}
|
||
confirmLoading={saving}
|
||
>
|
||
<Form form={resetForm} layout="vertical" style={{ marginTop: 16 }}>
|
||
<Form.Item label="新密码" name="newPassword" rules={[{ required: true, message: '请输入新密码' }, { min: 6, message: '密码至少 6 位' }]}>
|
||
<Input.Password />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default UserManagement;
|