208 lines
9.5 KiB
TypeScript
208 lines
9.5 KiB
TypeScript
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';
|
||
import './ClientUsersPage.css';
|
||
|
||
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('client');
|
||
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 client-user-actions" aria-label={`${record.displayName}的用户操作`}>
|
||
<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={16} />} onClick={() => openEditor()} size="sm">添加用户</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 client-users-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>
|
||
);
|
||
}
|