feat: complete login and user management flow

This commit is contained in:
hectorzhao
2026-07-02 16:28:48 +08:00
parent 9c639d54b9
commit e26d8324c3
22 changed files with 1320 additions and 300 deletions
+185 -131
View File
@@ -1,155 +1,166 @@
import { useMemo, useState } from 'react';
import { Plus, Search } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { KeyRound, Plus, Search } from 'lucide-react';
import { adminApi, type ManagedUser, type TenantOption, type UserPayload } from '@/api/adminApi';
import { readSession } from '@/api/session';
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
type UserStatus = 'enabled' | 'disabled';
type AdminUser = {
id: string;
name: string;
account: string;
role: string;
type UserForm = {
tenantId: string;
displayName: string;
username: string;
email: string;
phone: string;
status: UserStatus;
createdAt: string;
lastLogin: string;
roleCode: 'platform_admin' | 'enterprise_admin';
status: string;
password: string;
};
const statusLabelMap: Record<UserStatus, string> = {
enabled: '启用',
disabled: '停用',
type ConfirmAction = {
type: 'status' | 'delete';
user: ManagedUser;
};
const statusToneMap: Record<UserStatus, 'success' | 'neutral'> = {
enabled: 'success',
disabled: 'neutral',
const emptyForm: UserForm = {
tenantId: '',
displayName: '',
username: '',
email: '',
phone: '',
roleCode: 'platform_admin',
status: 'active',
password: '',
};
const initialUsers: AdminUser[] = [
{ id: 'USR20260630001', name: '李明', account: 'liming', role: '平台管理员', phone: '13800001234', status: 'enabled', createdAt: '2026-06-01 09:20:10', lastLogin: '2026-06-30 09:15:22' },
{ id: 'USR20260630002', name: '张青', account: 'zhangqing', role: '审核专员', phone: '13900005678', status: 'enabled', createdAt: '2026-06-03 14:05:36', lastLogin: '2026-06-29 18:22:11' },
{ id: 'USR20260630003', name: '王珊', account: 'wangshan', role: '运营人员', phone: '13755558888', status: 'disabled', createdAt: '2026-06-08 11:12:48', lastLogin: '2026-06-21 10:08:09' },
{ id: 'USR20260630004', name: '赵一', account: 'zhaoyi', role: '财务人员', phone: '13677779999', status: 'enabled', createdAt: '2026-06-12 16:30:00', lastLogin: '2026-06-30 08:40:18' },
];
function createUserId() {
return `USR${Date.now()}`;
}
type UserFormModalProps = {
item?: AdminUser;
onClose: () => void;
onSubmit: (item: AdminUser) => void;
const roleLabel: Record<string, string> = {
platform_admin: '平台管理员',
enterprise_admin: '企业管理员',
};
function UserFormModal({ item, onClose, onSubmit }: UserFormModalProps) {
const [form, setForm] = useState<AdminUser>(() => item ?? {
id: createUserId(),
name: '',
account: '',
role: '运营人员',
phone: '',
status: 'enabled',
createdAt: '2026-06-30 10:00:00',
lastLogin: '-',
});
function updateField<Key extends keyof AdminUser>(key: Key, value: AdminUser[Key]) {
setForm((current) => ({ ...current, [key]: value }));
}
function handleSubmit() {
onSubmit(form);
}
return (
<Modal
footer={(
<>
<Button onClick={onClose} variant="ghost"></Button>
<Button onClick={handleSubmit}></Button>
</>
)}
onClose={onClose}
open
title={item ? '编辑用户' : '新增用户'}
>
<div className="admin-system-modal-form">
<Input label="用户姓名" onChange={(event) => updateField('name', event.target.value)} value={form.name} />
<Input label="登录账号" onChange={(event) => updateField('account', event.target.value)} value={form.account} />
<Select
label="角色"
onChange={(event) => updateField('role', event.target.value)}
options={[
{ label: '平台管理员', value: '平台管理员' },
{ label: '审核专员', value: '审核专员' },
{ label: '运营人员', value: '运营人员' },
{ label: '财务人员', value: '财务人员' },
]}
value={form.role}
/>
<Input label="手机号码" onChange={(event) => updateField('phone', event.target.value)} value={form.phone} />
<Select
label="状态"
onChange={(event) => updateField('status', event.target.value as UserStatus)}
options={[
{ label: '启用', value: 'enabled' },
{ label: '停用', value: 'disabled' },
]}
value={form.status}
/>
</div>
</Modal>
);
function toForm(user?: ManagedUser): UserForm {
const roleCode = user?.roles[0]?.role.code === 'enterprise_admin' ? 'enterprise_admin' : 'platform_admin';
return user ? {
tenantId: user.tenantId ?? '',
displayName: user.displayName,
username: user.username,
email: user.email ?? '',
phone: user.phone ?? '',
roleCode,
status: user.status,
password: '',
} : emptyForm;
}
export function AdminUsersPage() {
const [users, setUsers] = useState(initialUsers);
const session = readSession();
const [users, setUsers] = useState<ManagedUser[]>([]);
const [tenants, setTenants] = useState<TenantOption[]>([]);
const [keyword, setKeyword] = useState('');
const [editingUser, setEditingUser] = useState<AdminUser | null>(null);
const [editingUser, setEditingUser] = useState<ManagedUser | null>(null);
const [creating, setCreating] = useState(false);
const [form, setForm] = useState<UserForm>(emptyForm);
const [passwordUser, setPasswordUser] = useState<ManagedUser | null>(null);
const [newPassword, setNewPassword] = useState('');
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null);
const [error, setError] = useState('');
const filteredUsers = useMemo(
() => users.filter((user) => [user.name, user.account, user.role, user.phone].some((value) => value.includes(keyword))),
[keyword, users],
);
function upsertUser(nextUser: AdminUser) {
setUsers((current) => {
const exists = current.some((item) => item.id === nextUser.id);
if (exists) {
return current.map((item) => (item.id === nextUser.id ? nextUser : item));
}
return [nextUser, ...current];
});
setEditingUser(null);
setCreating(false);
async function load() {
const [nextUsers, nextTenants] = await Promise.all([adminApi.listUsers(), adminApi.listTenants()]);
setUsers(nextUsers);
setTenants(nextTenants);
}
const columns = useMemo<Array<TableColumn<AdminUser>>>(() => [
{ key: 'name', title: '用户姓名', width: '150px', render: (record) => <strong>{record.name}</strong> },
{ key: 'account', title: '登录账号', width: '160px', render: (record) => record.account },
{ key: 'role', title: '角色', width: '150px', render: (record) => record.role },
{ key: 'phone', title: '手机号码', width: '150px', render: (record) => record.phone },
{ key: 'status', title: '状态', width: '120px', render: (record) => <Tag tone={statusToneMap[record.status]}>{statusLabelMap[record.status]}</Tag> },
{ key: 'createdAt', title: '创建时间', width: '190px', render: (record) => record.createdAt },
{ key: 'lastLogin', title: '最近登录', width: '190px', render: (record) => record.lastLogin },
useEffect(() => {
void load().catch((err) => setError(err instanceof Error ? err.message : '加载用户失败'));
}, []);
const filteredUsers = useMemo(() => {
const value = keyword.trim().toLowerCase();
return users.filter((user) => {
const target = `${user.displayName} ${user.username} ${user.email ?? ''} ${user.phone ?? ''} ${user.tenant?.name ?? ''} ${roleLabel[user.roles[0]?.role.code] ?? ''}`.toLowerCase();
return !value || target.includes(value);
});
}, [keyword, users]);
function openCreate() {
setForm({ ...emptyForm, tenantId: tenants[0]?.id ?? '' });
setCreating(true);
}
function openEdit(user: ManagedUser) {
setForm(toForm(user));
setEditingUser(user);
}
function updateField<Key extends keyof UserForm>(key: Key, value: UserForm[Key]) {
setForm((current) => {
const next = { ...current, [key]: value };
if (key === 'roleCode' && value === 'platform_admin') {
next.tenantId = '';
}
return next;
});
}
async function saveUser() {
setError('');
const body: UserPayload = {
tenantId: form.roleCode === 'enterprise_admin' ? form.tenantId : null,
username: form.username || form.email || form.phone,
email: form.email,
phone: form.phone,
displayName: form.displayName,
status: form.status,
roleCode: form.roleCode,
operatorId: session?.user.id,
};
if (creating) {
await adminApi.createUser({ ...body, password: form.password });
} else if (editingUser) {
await adminApi.updateUser(editingUser.id, body);
}
setCreating(false);
setEditingUser(null);
await load();
}
async function runConfirm() {
if (!confirmAction) return;
if (confirmAction.type === 'delete') {
await adminApi.deleteUser(confirmAction.user.id, session?.user.id);
} else {
await adminApi.changeUserStatus(confirmAction.user.id, confirmAction.user.status === 'active' ? 'disabled' : 'active', session?.user.id);
}
setConfirmAction(null);
await load();
}
async function savePassword() {
if (!passwordUser) return;
await adminApi.changeUserPassword(passwordUser.id, newPassword, session?.user.id);
setPasswordUser(null);
setNewPassword('');
}
const columns = useMemo<Array<TableColumn<ManagedUser>>>(() => [
{ key: 'displayName', title: '用户姓名', width: '140px', render: (record) => <strong>{record.displayName}</strong> },
{ key: 'account', title: '邮箱/手机号', width: '230px', render: (record) => <span>{record.email ?? '-'}<br /><small className="muted">{record.phone ?? '-'}</small></span> },
{ key: 'role', title: '角色', width: '130px', render: (record) => roleLabel[record.roles[0]?.role.code] ?? record.roles[0]?.role.name ?? '-' },
{ key: 'tenant', title: '企业', width: '180px', render: (record) => record.tenant?.name ?? '-' },
{ key: 'status', title: '状态', width: '110px', render: (record) => <Tag tone={record.status === 'active' ? 'success' : 'neutral'}>{record.status === 'active' ? '启用' : '禁用'}</Tag> },
{ key: 'lastLoginAt', title: '最近登录', width: '190px', render: (record) => record.lastLoginAt ? new Date(record.lastLoginAt).toLocaleString('zh-CN') : '-' },
{
key: 'actions',
title: '操作',
width: '170px',
width: '280px',
align: 'right',
render: (record) => (
<div className="admin-system-actions">
<Button onClick={() => setEditingUser(record)} size="sm" variant="ghost"></Button>
<Button
onClick={() => setUsers((current) => current.filter((item) => item.id !== record.id))}
size="sm"
variant="danger"
>
<Button onClick={() => openEdit(record)} size="sm" variant="ghost"></Button>
<Button onClick={() => { setPasswordUser(record); setNewPassword(''); }} size="sm" variant="ghost"></Button>
<Button onClick={() => setConfirmAction({ type: 'status', user: record })} size="sm" variant="ghost">
{record.status === 'active' ? '禁用' : '启用'}
</Button>
<Button onClick={() => setConfirmAction({ type: 'delete', user: record })} size="sm" variant="danger"></Button>
</div>
),
},
@@ -165,16 +176,59 @@ export function AdminUsersPage() {
</div>
<div className="surface admin-system-toolbar">
<Input onChange={(event) => setKeyword(event.target.value)} placeholder="搜索姓名、号、角色或手机号" prefix={<Search size={16} />} value={keyword} />
<Button icon={<Plus size={16} />} onClick={() => setCreating(true)}></Button>
<Input onChange={(event) => setKeyword(event.target.value)} placeholder="搜索姓名、邮箱、手机号、角色或企业" prefix={<Search size={16} />} value={keyword} />
<Button icon={<Plus size={16} />} onClick={openCreate}></Button>
</div>
{error ? <div className="surface empty-state">{error}</div> : null}
<div className="surface admin-system-table-card">
<Table columns={columns} data={filteredUsers} emptyText="暂无用户" rowKey="id" />
</div>
{creating ? <UserFormModal onClose={() => setCreating(false)} onSubmit={upsertUser} /> : null}
{editingUser ? <UserFormModal item={editingUser} onClose={() => setEditingUser(null)} onSubmit={upsertUser} /> : null}
{(creating || editingUser) ? (
<Modal
footer={<><Button onClick={() => { setCreating(false); setEditingUser(null); }} variant="ghost"></Button><Button onClick={() => void saveUser()}></Button></>}
onClose={() => { setCreating(false); setEditingUser(null); }}
open
title={creating ? '新增用户' : '编辑用户'}
>
<div className="admin-system-modal-form">
<Input label="用户姓名" onChange={(event) => updateField('displayName', event.target.value)} value={form.displayName} />
<Input label="邮箱" onChange={(event) => updateField('email', event.target.value)} value={form.email} />
<Input label="手机号" onChange={(event) => updateField('phone', event.target.value)} value={form.phone} />
<Input label="登录账号" onChange={(event) => updateField('username', event.target.value)} value={form.username} />
<Select
label="用户类型"
onChange={(event) => updateField('roleCode', event.target.value as UserForm['roleCode'])}
options={[{ label: '平台管理员', value: 'platform_admin' }, { label: '企业管理员', value: 'enterprise_admin' }]}
value={form.roleCode}
/>
{form.roleCode === 'enterprise_admin' ? (
<Select
label="关联企业"
onChange={(event) => updateField('tenantId', event.target.value)}
options={tenants.map((tenant) => ({ label: tenant.name, value: tenant.id }))}
value={form.tenantId}
/>
) : null}
{creating ? <Input label="初始密码" onChange={(event) => updateField('password', event.target.value)} type="password" value={form.password} /> : null}
<Select label="状态" onChange={(event) => updateField('status', event.target.value)} options={[{ label: '启用', value: 'active' }, { label: '禁用', value: 'disabled' }]} value={form.status} />
</div>
</Modal>
) : null}
{passwordUser ? (
<Modal footer={<><Button onClick={() => setPasswordUser(null)} variant="ghost"></Button><Button icon={<KeyRound size={16} />} onClick={() => void savePassword()}></Button></>} onClose={() => setPasswordUser(null)} open title="修改密码">
<div className="admin-system-modal-form">
<Input label="新密码" onChange={(event) => setNewPassword(event.target.value)} type="password" value={newPassword} />
</div>
</Modal>
) : null}
{confirmAction ? (
<Modal footer={<><Button onClick={() => setConfirmAction(null)} variant="ghost"></Button><Button onClick={() => void runConfirm()} variant={confirmAction.type === 'delete' ? 'danger' : 'primary'}></Button></>} onClose={() => setConfirmAction(null)} open title={confirmAction.type === 'delete' ? '删除用户' : '变更用户状态'}>
<p>{confirmAction.type === 'delete' ? `确认删除用户 ${confirmAction.user.displayName}` : `确认${confirmAction.user.status === 'active' ? '禁用' : '启用'}用户 ${confirmAction.user.displayName}`}</p>
</Modal>
) : null}
</section>
);
}