feat(auth): add org/dept/position management, user page, and Phase 2 completion
Complete Phase 2 identity & authentication module: - Organization CRUD with tree structure (parent_id + materialized path) - Department CRUD nested under organizations with tree support - Position CRUD nested under departments - User management page with table, create/edit modal, role assignment - Organization architecture page with 3-panel tree layout - Frontend API layer for orgs/depts/positions - Sidebar navigation updated with organization menu item - Fix parse_ttl edge case for strings ending in 'd' (e.g. "invalid")
This commit is contained in:
363
apps/web/src/pages/Users.tsx
Normal file
363
apps/web/src/pages/Users.tsx
Normal file
@@ -0,0 +1,363 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Tag,
|
||||
Popconfirm,
|
||||
Checkbox,
|
||||
message,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
listUsers,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
assignRoles,
|
||||
type CreateUserRequest,
|
||||
type UpdateUserRequest,
|
||||
} from '../api/users';
|
||||
import { listRoles, type RoleInfo } from '../api/roles';
|
||||
import type { UserInfo } from '../api/auth';
|
||||
|
||||
const STATUS_COLOR_MAP: Record<string, string> = {
|
||||
active: 'green',
|
||||
disabled: 'red',
|
||||
locked: 'orange',
|
||||
};
|
||||
|
||||
const STATUS_LABEL_MAP: Record<string, string> = {
|
||||
active: '正常',
|
||||
disabled: '禁用',
|
||||
locked: '锁定',
|
||||
};
|
||||
|
||||
export default function Users() {
|
||||
const [users, setUsers] = useState<UserInfo[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [editUser, setEditUser] = useState<UserInfo | null>(null);
|
||||
const [roleModalOpen, setRoleModalOpen] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState<UserInfo | null>(null);
|
||||
const [allRoles, setAllRoles] = useState<RoleInfo[]>([]);
|
||||
const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>([]);
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const fetchUsers = useCallback(async (p = page) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await listUsers(p, 20);
|
||||
setUsers(result.data);
|
||||
setTotal(result.total);
|
||||
} catch {
|
||||
message.error('加载用户列表失败');
|
||||
}
|
||||
setLoading(false);
|
||||
}, [page]);
|
||||
|
||||
const fetchRoles = useCallback(async () => {
|
||||
try {
|
||||
const result = await listRoles();
|
||||
setAllRoles(result.data);
|
||||
} catch {
|
||||
// Roles may not be seeded yet; silently ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
fetchRoles();
|
||||
}, [fetchUsers, fetchRoles]);
|
||||
|
||||
const handleCreateOrEdit = async (values: {
|
||||
username: string;
|
||||
password?: string;
|
||||
display_name?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}) => {
|
||||
try {
|
||||
if (editUser) {
|
||||
const req: UpdateUserRequest = {
|
||||
display_name: values.display_name,
|
||||
email: values.email,
|
||||
phone: values.phone,
|
||||
};
|
||||
await updateUser(editUser.id, req);
|
||||
message.success('用户更新成功');
|
||||
} else {
|
||||
const req: CreateUserRequest = {
|
||||
username: values.username,
|
||||
password: values.password ?? '',
|
||||
display_name: values.display_name,
|
||||
email: values.email,
|
||||
phone: values.phone,
|
||||
};
|
||||
await createUser(req);
|
||||
message.success('用户创建成功');
|
||||
}
|
||||
setCreateModalOpen(false);
|
||||
setEditUser(null);
|
||||
form.resetFields();
|
||||
fetchUsers();
|
||||
} catch (err: unknown) {
|
||||
const errorMsg =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data
|
||||
?.message || '操作失败';
|
||||
message.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteUser(id);
|
||||
message.success('用户已删除');
|
||||
fetchUsers();
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleStatus = async (id: string, status: string) => {
|
||||
try {
|
||||
await updateUser(id, { status });
|
||||
message.success(status === 'disabled' ? '用户已禁用' : '用户已启用');
|
||||
fetchUsers();
|
||||
} catch {
|
||||
message.error('状态更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssignRoles = async () => {
|
||||
if (!selectedUser) return;
|
||||
try {
|
||||
await assignRoles(selectedUser.id, selectedRoleIds);
|
||||
message.success('角色分配成功');
|
||||
setRoleModalOpen(false);
|
||||
fetchUsers();
|
||||
} catch {
|
||||
message.error('角色分配失败');
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
setEditUser(null);
|
||||
form.resetFields();
|
||||
setCreateModalOpen(true);
|
||||
};
|
||||
|
||||
const openEditModal = (user: UserInfo) => {
|
||||
setEditUser(user);
|
||||
form.setFieldsValue({
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
});
|
||||
setCreateModalOpen(true);
|
||||
};
|
||||
|
||||
const closeCreateModal = () => {
|
||||
setCreateModalOpen(false);
|
||||
setEditUser(null);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const openRoleModal = (user: UserInfo) => {
|
||||
setSelectedUser(user);
|
||||
setSelectedRoleIds(user.roles.map((r) => r.id));
|
||||
setRoleModalOpen(true);
|
||||
};
|
||||
|
||||
const filteredUsers = searchText
|
||||
? users.filter((u) =>
|
||||
u.username.toLowerCase().includes(searchText.toLowerCase()),
|
||||
)
|
||||
: users;
|
||||
|
||||
const columns = [
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{
|
||||
title: '显示名',
|
||||
dataIndex: 'display_name',
|
||||
key: 'display_name',
|
||||
render: (v: string | undefined) => v || '-',
|
||||
},
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||
{ title: '电话', dataIndex: 'phone', key: 'phone' },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<Tag color={STATUS_COLOR_MAP[status] || 'default'}>
|
||||
{STATUS_LABEL_MAP[status] || status}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'roles',
|
||||
key: 'roles',
|
||||
render: (roles: RoleInfo[]) =>
|
||||
roles.length > 0
|
||||
? roles.map((r) => <Tag key={r.id}>{r.name}</Tag>)
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render: (_: unknown, record: UserInfo) => (
|
||||
<Space>
|
||||
<Button size="small" onClick={() => openEditModal(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button size="small" onClick={() => openRoleModal(record)}>
|
||||
分配角色
|
||||
</Button>
|
||||
{record.status === 'active' ? (
|
||||
<Popconfirm
|
||||
title="确定禁用此用户?"
|
||||
onConfirm={() => handleToggleStatus(record.id, 'disabled')}
|
||||
>
|
||||
<Button size="small" danger>
|
||||
禁用
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleToggleStatus(record.id, 'active')}
|
||||
>
|
||||
启用
|
||||
</Button>
|
||||
)}
|
||||
<Popconfirm
|
||||
title="确定删除此用户?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button size="small" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
用户管理
|
||||
</Typography.Title>
|
||||
<Space>
|
||||
<Input
|
||||
placeholder="搜索用户名"
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={openCreateModal}
|
||||
>
|
||||
新建用户
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredUsers}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
total,
|
||||
pageSize: 20,
|
||||
onChange: (p) => {
|
||||
setPage(p);
|
||||
fetchUsers(p);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editUser ? '编辑用户' : '新建用户'}
|
||||
open={createModalOpen}
|
||||
onCancel={closeCreateModal}
|
||||
onOk={() => form.submit()}
|
||||
>
|
||||
<Form form={form} onFinish={handleCreateOrEdit} layout="vertical">
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
<Input disabled={!!editUser} />
|
||||
</Form.Item>
|
||||
{!editUser && (
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6位' },
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="display_name" label="显示名">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="邮箱"
|
||||
rules={[{ type: 'email', message: '请输入有效的邮箱地址' }]}
|
||||
>
|
||||
<Input type="email" />
|
||||
</Form.Item>
|
||||
<Form.Item name="phone" label="电话">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`分配角色 - ${selectedUser?.username || ''}`}
|
||||
open={roleModalOpen}
|
||||
onCancel={() => setRoleModalOpen(false)}
|
||||
onOk={handleAssignRoles}
|
||||
>
|
||||
<Checkbox.Group
|
||||
value={selectedRoleIds}
|
||||
onChange={(values) => setSelectedRoleIds(values as string[])}
|
||||
options={allRoles.map((r) => ({
|
||||
label: `${r.name} (${r.code})`,
|
||||
value: r.id,
|
||||
}))}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user