feat: complete phase2 baseline cdr quality rbac

This commit is contained in:
hectorzhao
2026-06-24 18:40:21 +08:00
parent 7057fd3c42
commit a86de6545f
63 changed files with 7853 additions and 3678 deletions
+131
View File
@@ -0,0 +1,131 @@
import { useMemo, useState } from 'react';
import { Alert, Badge, Button, Field, Input, Select } from '../components/ui.jsx';
import { Icon, PageTitle, Toolbar, Panel, ApiNotice, Modal, ConfirmDialog, SimpleTable } from '../components/layout.jsx';
import { explainApiError } from '../api.js';
export function UsersPage({ userRows, setUserRows, roleRows, apiLoading, apiError, refreshApi, can = () => true, onDeleteUser }) {
const [keyword, setKeyword] = useState('');
const [roleFilter, setRoleFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [editingUser, setEditingUser] = useState(undefined);
const [userForm, setUserForm] = useState(emptyUserForm);
const [resetTarget, setResetTarget] = useState(null);
const [deleteUserTarget, setDeleteUserTarget] = useState(null);
const [resetMessage, setResetMessage] = useState('');
const [actionError, setActionError] = useState('');
const canManage = can('users.manage');
const roleMap = useMemo(() => new Map(roleRows.map((role) => [role.id, role.name])), [roleRows]);
const visibleUsers = useMemo(() => {
const normalized = keyword.trim().toLowerCase();
return userRows.filter((user) => {
const matchesKeyword = !normalized || [user.username, user.name, user.phone, user.email].some((value) => value.toLowerCase().includes(normalized));
return matchesKeyword && (roleFilter === 'all' || user.roleId === roleFilter) && (statusFilter === 'all' || user.status === statusFilter);
});
}, [keyword, roleFilter, statusFilter, userRows]);
const openCreateUser = () => {
setEditingUser(null);
setUserForm(emptyUserForm);
};
const openEditUser = (user) => {
setEditingUser(user);
setUserForm({ username: user.username, name: user.name, phone: user.phone, email: user.email, roleId: user.roleId, status: user.status });
};
const closeUserModal = () => {
setEditingUser(undefined);
setUserForm(emptyUserForm);
};
const submitUser = (event) => {
event.preventDefault();
if (!userForm.username.trim() || !userForm.name.trim() || !userForm.roleId) return;
if (editingUser) {
setUserRows((rows) => rows.map((user) => user.id === editingUser.id ? { ...user, ...userForm } : user));
} else {
const nextId = `U${String(1001 + userRows.length).padStart(4, '0')}`;
setUserRows((rows) => [...rows, { id: nextId, ...userForm, lastLogin: '从未登录', lastIp: '-' }]);
}
closeUserModal();
};
const toggleUserStatus = (user) => {
setUserRows((rows) => rows.map((item) => item.id === user.id ? { ...item, status: item.status === '启用' ? '禁用' : '启用' } : item));
};
const deleteUser = async (user) => {
try {
setActionError('');
if (onDeleteUser) {
await onDeleteUser(user.id);
} else {
setUserRows((rows) => rows.filter((item) => item.id !== user.id));
}
setDeleteUserTarget(null);
} catch (error) {
setActionError(explainApiError(error));
}
};
const confirmResetPassword = () => {
setResetMessage(`${resetTarget.name}${resetTarget.username})的密码已重置,下次登录需修改密码。`);
setResetTarget(null);
};
return (
<>
<PageTitle title="用户管理" desc="管理运营端登录用户、所属角色、账号状态和登录安全。" actions={canManage ? <Button icon={<Icon type="plus" />} onClick={openCreateUser}>新增用户</Button> : null} />
<ApiNotice loading={apiLoading} error={apiError} onRetry={refreshApi} />
{resetMessage ? <Alert title="密码重置完成" tone="success">{resetMessage}</Alert> : null}
{actionError ? <Alert title="操作失败" tone="danger">{actionError}</Alert> : null}
<Toolbar>
<Field label="用户信息"><Input value={keyword} onChange={(event) => setKeyword(event.target.value)} placeholder="用户名、姓名、手机号或邮箱" /></Field>
<Field label="角色"><Select value={roleFilter} onChange={(event) => setRoleFilter(event.target.value)}><option value="all">全部角色</option>{roleRows.map((role) => <option key={role.id} value={role.id}>{role.name}</option>)}</Select></Field>
<Field label="状态"><Select value={statusFilter} onChange={(event) => setStatusFilter(event.target.value)}><option value="all">全部状态</option><option value="启用">启用</option><option value="禁用">禁用</option></Select></Field>
<Button variant="outline" onClick={() => { setKeyword(''); setRoleFilter('all'); setStatusFilter('all'); }}>重置</Button>
</Toolbar>
<Panel title="用户列表" aside={<Badge tone="neutral"> {visibleUsers.length} 个用户</Badge>} className="wide-panel">
<SimpleTable rows={visibleUsers.map((user) => ({ ...user, roleName: roleMap.get(user.roleId) || '-' }))} columns={[
{ key: 'username', label: '用户名' },
{ key: 'name', label: '姓名' },
{ key: 'phone', label: '手机号' },
{ key: 'roleName', label: '角色' },
{ key: 'status', label: '状态', status: true },
{ key: 'lastLogin', label: '最后登录时间' },
{ key: 'lastIp', label: '最后登录 IP' },
{ key: 'actions', label: '操作', render: (row) => <div className="table-actions">{canManage ? <Button size="sm" variant="outline" onClick={() => openEditUser(row)}>编辑</Button> : null}{canManage ? <Button size="sm" variant={row.status === '启用' ? 'ghost' : 'secondary'} onClick={() => toggleUserStatus(row)}>{row.status === '启用' ? '禁用' : '启用'}</Button> : null}{canManage ? <Button size="sm" variant="outline" onClick={() => setResetTarget(row)}>重置密码</Button> : null}{canManage ? <Button size="sm" variant="danger" onClick={() => setDeleteUserTarget(row)}>删除</Button> : '-'}</div> },
]} />
</Panel>
{editingUser !== undefined ? (
<Modal title={editingUser ? '编辑用户' : '新增用户'} onClose={closeUserModal}>
<form className="modal-form" onSubmit={submitUser}>
<div className="form-grid admin-form-grid">
<Field label={<span>用户名 <span className="required-star">*</span></span>}><Input value={userForm.username} onChange={(event) => setUserForm({ ...userForm, username: event.target.value })} placeholder="用于登录,不可重复" disabled={Boolean(editingUser)} required /></Field>
<Field label={<span>姓名 <span className="required-star">*</span></span>}><Input value={userForm.name} onChange={(event) => setUserForm({ ...userForm, name: event.target.value })} placeholder="请输入用户姓名" required /></Field>
<Field label="手机号"><Input value={userForm.phone} onChange={(event) => setUserForm({ ...userForm, phone: event.target.value })} placeholder="请输入手机号" /></Field>
<Field label="邮箱"><Input type="email" value={userForm.email} onChange={(event) => setUserForm({ ...userForm, email: event.target.value })} placeholder="请输入邮箱" /></Field>
<Field label={<span>角色 <span className="required-star">*</span></span>}><Select value={userForm.roleId} onChange={(event) => setUserForm({ ...userForm, roleId: event.target.value })} required>{roleRows.filter((role) => role.status === '启用').map((role) => <option key={role.id} value={role.id}>{role.name}</option>)}</Select></Field>
<Field label="状态"><Select value={userForm.status} onChange={(event) => setUserForm({ ...userForm, status: event.target.value })}><option value="启用">启用</option><option value="禁用">禁用</option></Select></Field>
</div>
<div className="modal-actions"><Button type="button" variant="outline" onClick={closeUserModal}>取消</Button><Button type="submit">{editingUser ? '保存修改' : '保存用户'}</Button></div>
</form>
</Modal>
) : null}
{resetTarget ? (
<ConfirmDialog
title="确认重置密码"
confirmLabel="确认重置"
onCancel={() => setResetTarget(null)}
onConfirm={confirmResetPassword}
>
<p>确认重置用户{resetTarget.name}{resetTarget.username}的登录密码吗重置后该用户需要使用临时密码登录并立即修改密码</p>
</ConfirmDialog>
) : null}
{deleteUserTarget ? (
<ConfirmDialog
title="删除用户确认"
confirmLabel="确认删除"
confirmVariant="danger"
onCancel={() => setDeleteUserTarget(null)}
onConfirm={() => void deleteUser(deleteUserTarget)}
>
<p>确认删除用户{deleteUserTarget.name}{deleteUserTarget.username}删除后该用户将不能登录也不再出现在用户列表中</p>
</ConfirmDialog>
) : null}
</>
);
}