Files
lisglosips/apps/web/src/pages/UsersPage.jsx
T

132 lines
8.7 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}
</>
);
}