Files
lislgosms/src/apps/client/ClientUsersPage.tsx
T

207 lines
9.3 KiB
TypeScript
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 { useEffect, useMemo, useState } from 'react';
import { Edit3, KeyRound, Plus, Search, Trash2, Users } from 'lucide-react';
import { clientApi, type ManagedUser, type UserPayload } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
import { readSession } from '@/api/session';
import { Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
type UserForm = {
displayName: string;
username: string;
email: string;
phone: string;
status: string;
password: string;
};
type ConfirmAction = {
type: 'status' | 'delete';
user: ManagedUser;
};
const emptyForm: UserForm = {
displayName: '',
username: '',
email: '',
phone: '',
status: 'active',
password: '',
};
function toForm(user?: ManagedUser): UserForm {
return user ? {
displayName: user.displayName,
username: user.username,
email: user.email ?? '',
phone: user.phone ?? '',
status: user.status,
password: '',
} : emptyForm;
}
export function ClientUsersPage() {
const session = readSession();
const tenantId = session?.user.tenantId ?? undefined;
const [users, setUsers] = useState<ManagedUser[]>([]);
const [keyword, setKeyword] = useState('');
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 [saving, setSaving] = useState(false);
async function load() {
if (!tenantId) return;
setUsers(await clientApi.listUsers(tenantId));
}
useEffect(() => {
void load().catch((err) => setError(err instanceof Error ? err.message : '加载用户失败'));
}, [tenantId]);
const filteredUsers = useMemo(() => {
const value = keyword.trim().toLowerCase();
return users.filter((item) => {
const target = `${item.displayName} ${item.email ?? ''} ${item.phone ?? ''}`.toLowerCase();
return !value || target.includes(value);
});
}, [keyword, users]);
function openEditor(user?: ManagedUser) {
setForm(toForm(user));
setEditingUser(user ?? null);
setCreating(!user);
}
function updateField<Key extends keyof UserForm>(key: Key, value: UserForm[Key]) {
setForm((current) => ({ ...current, [key]: value }));
}
async function saveUser() {
if (!form.displayName.trim() || (!form.email.trim() && !form.phone.trim()) || (creating && form.password.length < 6)) {
setError('请填写姓名、邮箱或手机号;新增用户密码至少 6 位');
return;
}
setSaving(true);
const body: UserPayload = {
displayName: form.displayName,
username: form.username || form.email || form.phone,
email: form.email,
phone: form.phone,
status: form.status,
roleCode: 'enterprise_admin',
operatorId: session?.user.id,
};
try {
if (creating) {
await clientApi.createUser({ ...body, password: form.password }, tenantId);
} else if (editingUser) {
await clientApi.updateUser(editingUser.id, body, tenantId);
}
setCreating(false);
setEditingUser(null);
await load();
} catch (failure) {
setError(failure instanceof Error ? failure.message : '用户保存失败');
} finally {
setSaving(false);
}
}
async function runConfirm() {
if (!confirmAction) return;
if (confirmAction.type === 'delete') {
await clientApi.deleteUser(confirmAction.user.id, session?.user.id, tenantId);
} else {
await clientApi.changeUserStatus(confirmAction.user.id, confirmAction.user.status === 'active' ? 'disabled' : 'active', session?.user.id, tenantId);
}
setConfirmAction(null);
await load();
}
async function savePassword() {
if (!passwordUser) return;
await clientApi.changeUserPassword(passwordUser.id, newPassword, session?.user.id, tenantId);
setPasswordUser(null);
setNewPassword('');
}
const columns = useMemo<Array<TableColumn<ManagedUser>>>(() => [
{ key: 'name', title: '用户名', width: '140px', render: (record) => <strong className="text-strong">{record.displayName}</strong> },
{ key: 'email', title: '邮箱', render: (record) => <span className="muted">{record.email ?? '-'}</span> },
{ key: 'phone', title: '手机号', width: '160px', render: (record) => <span className="muted">{record.phone ?? '-'}</span> },
{ key: 'role', title: '角色', width: '130px', render: () => <Tag tone="info">企业管理员</Tag> },
{ key: 'status', title: '状态', width: '120px', render: (record) => <Tag tone={record.status === 'active' ? 'success' : 'neutral'}>{record.status === 'active' ? '正常' : '禁用'}</Tag> },
{ key: 'lastLoginAt', title: '最后登录时间', width: '190px', render: (record) => <span className="muted">{formatDateTime(record.lastLoginAt)}</span> },
{
key: 'actions',
title: '操作',
width: '290px',
render: (record) => (
<div className="inline-actions">
<Button icon={<Edit3 size={15} />} onClick={() => openEditor(record)} size="sm" variant="ghost">编辑</Button>
<Button icon={<KeyRound size={15} />} onClick={() => { setPasswordUser(record); setNewPassword(''); }} size="sm" variant="ghost">改密</Button>
<Button onClick={() => setConfirmAction({ type: 'status', user: record })} size="sm" variant={record.status === 'active' ? 'warning' : 'success'}>{record.status === 'active' ? '禁用' : '启用'}</Button>
<Button icon={<Trash2 size={15} />} onClick={() => setConfirmAction({ type: 'delete', user: record })} size="sm" variant="danger">删除</Button>
</div>
),
},
], []);
return (
<section className="page-stack system-page">
<div className="system-page-toolbar">
<div className="sms-send-title">
<span className="sms-send-title__icon"><Users size={22} /></span>
<h1>用户管理</h1>
</div>
<Button icon={<Plus size={18} />} onClick={() => openEditor()}>添加用户</Button>
</div>
<div className="system-filter-row">
<Input onChange={(event) => setKeyword(event.target.value)} placeholder="搜索用户名、邮箱或手机号" prefix={<Search size={16} />} value={keyword} />
</div>
{error ? <div className="surface empty-state">{error}</div> : null}
<div className="surface system-table-card">
<Table columns={columns} data={filteredUsers} emptyText="暂无用户" rowKey="id" />
</div>
{(creating || editingUser) ? (
<Modal
footer={<><Button disabled={saving} onClick={() => { setCreating(false); setEditingUser(null); }} variant="secondary">取消</Button><Button disabled={saving} onClick={() => void saveUser()}>{saving ? '保存中...' : '保存'}</Button></>}
onClose={() => { setCreating(false); setEditingUser(null); }}
open
size="xl"
title={creating ? '添加用户' : '编辑用户'}
>
<div className="system-user-form">
<Input label="用户名 *" onChange={(event) => updateField('displayName', event.target.value)} placeholder="请输入用户名" value={form.displayName} />
<Input label="邮箱 *" onChange={(event) => updateField('email', event.target.value)} placeholder="请输入邮箱" value={form.email} />
<Input label="手机号 *" onChange={(event) => updateField('phone', event.target.value)} placeholder="请输入手机号" value={form.phone} />
<Input hint="可用用户名、邮箱或手机号登录" label="用户名/登录账号" onChange={(event) => updateField('username', event.target.value)} value={form.username} />
{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="secondary">取消</Button><Button onClick={() => void savePassword()}>保存</Button></>} onClose={() => setPasswordUser(null)} open title="修改密码">
<div className="system-user-form">
<Input label="新密码" onChange={(event) => setNewPassword(event.target.value)} type="password" value={newPassword} />
</div>
</Modal>
) : null}
{confirmAction ? (
<Modal footer={<><Button onClick={() => setConfirmAction(null)} variant="secondary">取消</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>
);
}