118 lines
8.5 KiB
React
118 lines
8.5 KiB
React
import { useMemo, useState } from 'react';
|
||
import { Alert, Badge, Button, Checkbox, Field, Input, Select, Textarea } from '../components/ui.jsx';
|
||
import { Icon, PageTitle, Panel, ApiNotice, Modal, ConfirmDialog, Drawer, SimpleTable } from '../components/layout.jsx';
|
||
import { permissionGroups } from '../fixtures/devFixtures.js';
|
||
import { explainApiError } from '../api.js';
|
||
|
||
export function RolesPage({ roleRows, setRoleRows, userRows, apiLoading, apiError, refreshApi, can = () => true, onDeleteRole }) {
|
||
const [editingRole, setEditingRole] = useState(undefined);
|
||
const [roleForm, setRoleForm] = useState(emptyRoleForm);
|
||
const [permissionRole, setPermissionRole] = useState(null);
|
||
const [permissionDraft, setPermissionDraft] = useState([]);
|
||
const [deleteRoleTarget, setDeleteRoleTarget] = useState(null);
|
||
const [actionError, setActionError] = useState('');
|
||
const canManage = can('roles.manage');
|
||
const roleUserCounts = useMemo(() => userRows.reduce((counts, user) => ({ ...counts, [user.roleId]: (counts[user.roleId] || 0) + 1 }), {}), [userRows]);
|
||
const openCreateRole = () => { setEditingRole(null); setRoleForm(emptyRoleForm); };
|
||
const openEditRole = (role) => { setEditingRole(role); setRoleForm({ name: role.name, description: role.description, status: role.status }); };
|
||
const closeRoleModal = () => { setEditingRole(undefined); setRoleForm(emptyRoleForm); };
|
||
const submitRole = (event) => {
|
||
event.preventDefault();
|
||
if (!roleForm.name.trim()) return;
|
||
if (editingRole) {
|
||
setRoleRows((rows) => rows.map((role) => role.id === editingRole.id ? { ...role, ...roleForm } : role));
|
||
} else {
|
||
setRoleRows((rows) => [...rows, { id: `R${String(rows.length + 1).padStart(3, '0')}`, ...roleForm, builtIn: false, permissions: [] }]);
|
||
}
|
||
closeRoleModal();
|
||
};
|
||
const toggleRoleStatus = (role) => {
|
||
if (role.builtIn) return;
|
||
setRoleRows((rows) => rows.map((item) => item.id === role.id ? { ...item, status: item.status === '启用' ? '禁用' : '启用' } : item));
|
||
};
|
||
const deleteRole = async (role) => {
|
||
try {
|
||
setActionError('');
|
||
if (onDeleteRole) {
|
||
await onDeleteRole(role.id);
|
||
} else {
|
||
setRoleRows((rows) => rows.filter((item) => item.id !== role.id));
|
||
}
|
||
setDeleteRoleTarget(null);
|
||
} catch (error) {
|
||
setActionError(explainApiError(error));
|
||
}
|
||
};
|
||
const openPermissions = (role) => { setPermissionRole(role); setPermissionDraft(role.permissions); };
|
||
const togglePermission = (permissionKey) => {
|
||
setPermissionDraft((permissions) => permissions.includes(permissionKey) ? permissions.filter((key) => key !== permissionKey) : [...permissions, permissionKey]);
|
||
};
|
||
const togglePermissionGroup = (group) => {
|
||
const groupKeys = group.permissions.map((permission) => permission.key);
|
||
const allSelected = groupKeys.every((key) => permissionDraft.includes(key));
|
||
setPermissionDraft((permissions) => allSelected ? permissions.filter((key) => !groupKeys.includes(key)) : Array.from(new Set([...permissions, ...groupKeys])));
|
||
};
|
||
const savePermissions = () => {
|
||
setRoleRows((rows) => rows.map((role) => role.id === permissionRole.id ? { ...role, permissions: permissionDraft } : role));
|
||
setPermissionRole(null);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<PageTitle title="角色与权限" desc="按岗位定义菜单与操作权限,并查看角色关联用户。" actions={canManage ? <Button icon={<Icon type="plus" />} onClick={openCreateRole}>新增角色</Button> : null} />
|
||
<ApiNotice loading={apiLoading} error={apiError} onRetry={refreshApi} />
|
||
{actionError ? <Alert title="操作失败" tone="danger">{actionError}</Alert> : null}
|
||
<Panel title="角色列表" aside={<Badge tone="neutral">{roleRows.length} 个角色</Badge>} className="wide-panel">
|
||
<SimpleTable rows={roleRows.map((role) => ({ ...role, type: role.builtIn ? '系统内置' : '自定义', userCount: role.userCount ?? roleUserCounts[role.id] ?? 0, permissionCount: role.permissions.length }))} columns={[
|
||
{ key: 'name', label: '角色名称' },
|
||
{ key: 'type', label: '类型' },
|
||
{ key: 'description', label: '角色说明' },
|
||
{ key: 'userCount', label: '用户数' },
|
||
{ key: 'permissionCount', label: '权限项' },
|
||
{ key: 'status', label: '状态', status: true },
|
||
{ key: 'actions', label: '操作', render: (row) => <div className="table-actions"><Button size="sm" variant="secondary" onClick={() => openPermissions(row)}>{canManage && !row.builtIn ? '配置权限' : '查看权限'}</Button>{canManage ? <Button size="sm" variant="outline" onClick={() => openEditRole(row)}>编辑</Button> : null}{canManage ? <Button size="sm" variant="ghost" disabled={row.builtIn} onClick={() => toggleRoleStatus(row)}>{row.status === '启用' ? '禁用' : '启用'}</Button> : null}{canManage ? <Button size="sm" variant="danger" disabled={row.builtIn} onClick={() => setDeleteRoleTarget(row)}>删除</Button> : null}</div> },
|
||
]} />
|
||
</Panel>
|
||
{editingRole !== undefined ? (
|
||
<Modal title={editingRole ? '编辑角色' : '新增角色'} onClose={closeRoleModal} size="sm">
|
||
<form className="modal-form" onSubmit={submitRole}>
|
||
<Field label={<span>角色名称 <span className="required-star">*</span></span>}><Input value={roleForm.name} onChange={(event) => setRoleForm({ ...roleForm, name: event.target.value })} placeholder="请输入角色名称" disabled={Boolean(editingRole?.builtIn)} required /></Field>
|
||
<Field label="角色说明"><Textarea rows="4" value={roleForm.description} onChange={(event) => setRoleForm({ ...roleForm, description: event.target.value })} placeholder="说明该角色的职责范围" /></Field>
|
||
<Field label="状态"><Select value={roleForm.status} onChange={(event) => setRoleForm({ ...roleForm, status: event.target.value })} disabled={Boolean(editingRole?.builtIn)}><option value="启用">启用</option><option value="禁用">禁用</option></Select></Field>
|
||
<div className="modal-actions"><Button type="button" variant="outline" onClick={closeRoleModal}>取消</Button><Button type="submit">保存角色</Button></div>
|
||
</form>
|
||
</Modal>
|
||
) : null}
|
||
{permissionRole ? (
|
||
<Drawer title={permissionRole.builtIn ? '查看角色权限' : '配置角色权限'} aside={<><Badge tone="info">{permissionRole.name}</Badge><span className="drawer-subtitle">已选择 {permissionDraft.length} 项权限</span></>} onClose={() => setPermissionRole(null)}>
|
||
{permissionRole.builtIn || !canManage ? <Alert title={permissionRole.builtIn ? '系统内置角色' : '只读权限'} tone="info">{permissionRole.builtIn ? '内置角色权限由系统维护,仅支持查看。' : '当前账号没有角色管理权限,仅支持查看。'}</Alert> : null}
|
||
<div className="permission-groups">
|
||
{permissionGroups.map((group) => {
|
||
const groupKeys = group.permissions.map((permission) => permission.key);
|
||
const selectedCount = groupKeys.filter((key) => permissionDraft.includes(key)).length;
|
||
return (
|
||
<section className="permission-group" key={group.name}>
|
||
<div className="permission-group-head"><div><strong>{group.name}</strong><span>{selectedCount}/{groupKeys.length} 已选择</span></div><Checkbox label="全选" checked={selectedCount === groupKeys.length} disabled={permissionRole.builtIn || !canManage} onChange={() => togglePermissionGroup(group)} /></div>
|
||
<div className="permission-options">{group.permissions.map((permission) => <Checkbox key={permission.key} label={permission.label} checked={permissionDraft.includes(permission.key)} disabled={permissionRole.builtIn || !canManage} onChange={() => togglePermission(permission.key)} />)}</div>
|
||
</section>
|
||
);
|
||
})}
|
||
</div>
|
||
<div className="drawer-actions"><Button variant="outline" onClick={() => setPermissionRole(null)}>关闭</Button>{permissionRole.builtIn || !canManage ? null : <Button onClick={savePermissions}>保存权限</Button>}</div>
|
||
</Drawer>
|
||
) : null}
|
||
{deleteRoleTarget ? (
|
||
<ConfirmDialog
|
||
title="删除角色确认"
|
||
confirmLabel="确认删除"
|
||
confirmVariant="danger"
|
||
onCancel={() => setDeleteRoleTarget(null)}
|
||
onConfirm={() => void deleteRole(deleteRoleTarget)}
|
||
>
|
||
<p>确认删除角色「{deleteRoleTarget.name}」吗?如果仍有用户使用该角色,系统会拒绝删除。</p>
|
||
</ConfirmDialog>
|
||
) : null}
|
||
</>
|
||
);
|
||
}
|