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:
iven
2026-04-11 04:00:32 +08:00
parent 6fd0288e7c
commit 8a012f6c6a
15 changed files with 2409 additions and 10 deletions

View File

@@ -6,6 +6,8 @@ import MainLayout from './layouts/MainLayout';
import Login from './pages/Login';
import Home from './pages/Home';
import Roles from './pages/Roles';
import Users from './pages/Users';
import Organizations from './pages/Organizations';
import { useAuthStore } from './stores/auth';
import { useAppStore } from './stores/app';
@@ -40,8 +42,9 @@ export default function App() {
<MainLayout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users" element={<div></div>} />
<Route path="/users" element={<Users />} />
<Route path="/roles" element={<Roles />} />
<Route path="/organizations" element={<Organizations />} />
<Route path="/settings" element={<div></div>} />
</Routes>
</MainLayout>

168
apps/web/src/api/orgs.ts Normal file
View File

@@ -0,0 +1,168 @@
import client from './client';
// --- Organization types ---
export interface OrganizationInfo {
id: string;
name: string;
code?: string;
parent_id?: string;
path?: string;
level: number;
sort_order: number;
children: OrganizationInfo[];
}
export interface CreateOrganizationRequest {
name: string;
code?: string;
parent_id?: string;
sort_order?: number;
}
export interface UpdateOrganizationRequest {
name?: string;
code?: string;
sort_order?: number;
}
// --- Department types ---
export interface DepartmentInfo {
id: string;
org_id: string;
name: string;
code?: string;
parent_id?: string;
manager_id?: string;
path?: string;
sort_order: number;
children: DepartmentInfo[];
}
export interface CreateDepartmentRequest {
name: string;
code?: string;
parent_id?: string;
manager_id?: string;
sort_order?: number;
}
export interface UpdateDepartmentRequest {
name?: string;
code?: string;
manager_id?: string;
sort_order?: number;
}
// --- Position types ---
export interface PositionInfo {
id: string;
dept_id: string;
name: string;
code?: string;
level: number;
sort_order: number;
}
export interface CreatePositionRequest {
name: string;
code?: string;
level?: number;
sort_order?: number;
}
export interface UpdatePositionRequest {
name?: string;
code?: string;
level?: number;
sort_order?: number;
}
// --- Organization API ---
export async function listOrgTree() {
const { data } = await client.get<{ success: boolean; data: OrganizationInfo[] }>(
'/organizations',
);
return data.data;
}
export async function createOrg(req: CreateOrganizationRequest) {
const { data } = await client.post<{ success: boolean; data: OrganizationInfo }>(
'/organizations',
req,
);
return data.data;
}
export async function updateOrg(id: string, req: UpdateOrganizationRequest) {
const { data } = await client.put<{ success: boolean; data: OrganizationInfo }>(
`/organizations/${id}`,
req,
);
return data.data;
}
export async function deleteOrg(id: string) {
await client.delete(`/organizations/${id}`);
}
// --- Department API ---
export async function listDeptTree(orgId: string) {
const { data } = await client.get<{ success: boolean; data: DepartmentInfo[] }>(
`/organizations/${orgId}/departments`,
);
return data.data;
}
export async function createDept(orgId: string, req: CreateDepartmentRequest) {
const { data } = await client.post<{ success: boolean; data: DepartmentInfo }>(
`/organizations/${orgId}/departments`,
req,
);
return data.data;
}
export async function deleteDept(id: string) {
await client.delete(`/departments/${id}`);
}
export async function updateDept(id: string, req: UpdateDepartmentRequest) {
const { data } = await client.put<{ success: boolean; data: DepartmentInfo }>(
`/departments/${id}`,
req,
);
return data.data;
}
// --- Position API ---
export async function listPositions(deptId: string) {
const { data } = await client.get<{ success: boolean; data: PositionInfo[] }>(
`/departments/${deptId}/positions`,
);
return data.data;
}
export async function createPosition(deptId: string, req: CreatePositionRequest) {
const { data } = await client.post<{ success: boolean; data: PositionInfo }>(
`/departments/${deptId}/positions`,
req,
);
return data.data;
}
export async function deletePosition(id: string) {
await client.delete(`/positions/${id}`);
}
export async function updatePosition(id: string, req: UpdatePositionRequest) {
const { data } = await client.put<{ success: boolean; data: PositionInfo }>(
`/positions/${id}`,
req,
);
return data.data;
}

View File

@@ -3,6 +3,7 @@ import {
HomeOutlined,
UserOutlined,
SafetyOutlined,
ApartmentOutlined,
BellOutlined,
SettingOutlined,
MenuFoldOutlined,
@@ -19,6 +20,7 @@ const menuItems = [
{ key: '/', icon: <HomeOutlined />, label: '首页' },
{ key: '/users', icon: <UserOutlined />, label: '用户管理' },
{ key: '/roles', icon: <SafetyOutlined />, label: '权限管理' },
{ key: '/organizations', icon: <ApartmentOutlined />, label: '组织架构' },
{ key: '/settings', icon: <SettingOutlined />, label: '系统设置' },
];

View File

@@ -0,0 +1,586 @@
import { useState, useEffect, useCallback } from 'react';
import {
Tree,
Button,
Space,
Modal,
Form,
Input,
InputNumber,
Table,
Popconfirm,
message,
Typography,
Card,
Empty,
Tag,
} from 'antd';
import {
PlusOutlined,
DeleteOutlined,
EditOutlined,
ApartmentOutlined,
} from '@ant-design/icons';
import type { DataNode } from 'antd/es/tree';
import {
listOrgTree,
createOrg,
updateOrg,
deleteOrg,
listDeptTree,
createDept,
deleteDept,
listPositions,
createPosition,
deletePosition,
type OrganizationInfo,
type DepartmentInfo,
type PositionInfo,
} from '../api/orgs';
export default function Organizations() {
// --- Org tree state ---
const [orgTree, setOrgTree] = useState<OrganizationInfo[]>([]);
const [selectedOrg, setSelectedOrg] = useState<OrganizationInfo | null>(null);
const [loading, setLoading] = useState(false);
// --- Department tree state ---
const [deptTree, setDeptTree] = useState<DepartmentInfo[]>([]);
const [selectedDept, setSelectedDept] = useState<DepartmentInfo | null>(null);
// --- Position list state ---
const [positions, setPositions] = useState<PositionInfo[]>([]);
// --- Modal state ---
const [orgModalOpen, setOrgModalOpen] = useState(false);
const [deptModalOpen, setDeptModalOpen] = useState(false);
const [positionModalOpen, setPositionModalOpen] = useState(false);
const [editOrg, setEditOrg] = useState<OrganizationInfo | null>(null);
const [orgForm] = Form.useForm();
const [deptForm] = Form.useForm();
const [positionForm] = Form.useForm();
// --- Fetch org tree ---
const fetchOrgTree = useCallback(async () => {
setLoading(true);
try {
const tree = await listOrgTree();
setOrgTree(tree);
// Clear selection if org no longer exists
if (selectedOrg) {
const stillExists = findOrgInTree(tree, selectedOrg.id);
if (!stillExists) {
setSelectedOrg(null);
setDeptTree([]);
setPositions([]);
}
}
} catch {
message.error('加载组织树失败');
}
setLoading(false);
}, [selectedOrg]);
useEffect(() => {
fetchOrgTree();
}, [fetchOrgTree]);
// --- Fetch dept tree when org selected ---
const fetchDeptTree = useCallback(async () => {
if (!selectedOrg) return;
try {
const tree = await listDeptTree(selectedOrg.id);
setDeptTree(tree);
if (selectedDept) {
const stillExists = findDeptInTree(tree, selectedDept.id);
if (!stillExists) {
setSelectedDept(null);
setPositions([]);
}
}
} catch {
message.error('加载部门树失败');
}
}, [selectedOrg, selectedDept]);
useEffect(() => {
fetchDeptTree();
}, [fetchDeptTree]);
// --- Fetch positions when dept selected ---
const fetchPositions = useCallback(async () => {
if (!selectedDept) return;
try {
const list = await listPositions(selectedDept.id);
setPositions(list);
} catch {
message.error('加载岗位列表失败');
}
}, [selectedDept]);
useEffect(() => {
fetchPositions();
}, [fetchPositions]);
// --- Org handlers ---
const handleCreateOrg = async (values: {
name: string;
code?: string;
sort_order?: number;
}) => {
try {
if (editOrg) {
await updateOrg(editOrg.id, {
name: values.name,
code: values.code,
sort_order: values.sort_order,
});
message.success('组织更新成功');
} else {
await createOrg({
name: values.name,
code: values.code,
parent_id: selectedOrg?.id,
sort_order: values.sort_order,
});
message.success('组织创建成功');
}
setOrgModalOpen(false);
setEditOrg(null);
orgForm.resetFields();
fetchOrgTree();
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '操作失败';
message.error(errorMsg);
}
};
const handleDeleteOrg = async (id: string) => {
try {
await deleteOrg(id);
message.success('组织已删除');
setSelectedOrg(null);
setDeptTree([]);
setPositions([]);
fetchOrgTree();
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '删除失败';
message.error(errorMsg);
}
};
// --- Dept handlers ---
const handleCreateDept = async (values: {
name: string;
code?: string;
sort_order?: number;
}) => {
if (!selectedOrg) return;
try {
await createDept(selectedOrg.id, {
name: values.name,
code: values.code,
parent_id: selectedDept?.id,
sort_order: values.sort_order,
});
message.success('部门创建成功');
setDeptModalOpen(false);
deptForm.resetFields();
fetchDeptTree();
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '操作失败';
message.error(errorMsg);
}
};
const handleDeleteDept = async (id: string) => {
try {
await deleteDept(id);
message.success('部门已删除');
setSelectedDept(null);
setPositions([]);
fetchDeptTree();
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '删除失败';
message.error(errorMsg);
}
};
// --- Position handlers ---
const handleCreatePosition = async (values: {
name: string;
code?: string;
level?: number;
sort_order?: number;
}) => {
if (!selectedDept) return;
try {
await createPosition(selectedDept.id, {
name: values.name,
code: values.code,
level: values.level,
sort_order: values.sort_order,
});
message.success('岗位创建成功');
setPositionModalOpen(false);
positionForm.resetFields();
fetchPositions();
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '操作失败';
message.error(errorMsg);
}
};
const handleDeletePosition = async (id: string) => {
try {
await deletePosition(id);
message.success('岗位已删除');
fetchPositions();
} catch {
message.error('删除失败');
}
};
// --- Tree node converters ---
const convertOrgTree = (items: OrganizationInfo[]): DataNode[] =>
items.map((item) => ({
key: item.id,
title: (
<span>
{item.name}{' '}
{item.code && <Tag color="blue" style={{ marginLeft: 4 }}>{item.code}</Tag>}
</span>
),
children: convertOrgTree(item.children),
}));
const convertDeptTree = (items: DepartmentInfo[]): DataNode[] =>
items.map((item) => ({
key: item.id,
title: (
<span>
{item.name}{' '}
{item.code && <Tag color="green" style={{ marginLeft: 4 }}>{item.code}</Tag>}
</span>
),
children: convertDeptTree(item.children),
}));
// --- Helper to find node in tree ---
const onSelectOrg = (selectedKeys: React.Key[]) => {
if (selectedKeys.length === 0) {
setSelectedOrg(null);
setDeptTree([]);
setSelectedDept(null);
setPositions([]);
return;
}
const org = findOrgInTree(orgTree, selectedKeys[0] as string);
setSelectedOrg(org);
setSelectedDept(null);
setPositions([]);
};
const onSelectDept = (selectedKeys: React.Key[]) => {
if (selectedKeys.length === 0) {
setSelectedDept(null);
setPositions([]);
return;
}
const dept = findDeptInTree(deptTree, selectedKeys[0] as string);
setSelectedDept(dept);
};
const positionColumns = [
{ title: '岗位名称', dataIndex: 'name', key: 'name' },
{ title: '编码', dataIndex: 'code', key: 'code', render: (v?: string) => v || '-' },
{ title: '级别', dataIndex: 'level', key: 'level' },
{ title: '排序', dataIndex: 'sort_order', key: 'sort_order' },
{
title: '操作',
key: 'actions',
render: (_: unknown, record: PositionInfo) => (
<Popconfirm
title="确定删除此岗位?"
onConfirm={() => handleDeletePosition(record.id)}
>
<Button size="small" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
),
},
];
return (
<div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 16,
}}
>
<Typography.Title level={4} style={{ margin: 0 }}>
<ApartmentOutlined style={{ marginRight: 8 }} />
</Typography.Title>
</div>
<div style={{ display: 'flex', gap: 16, minHeight: 500 }}>
{/* Left: Organization Tree */}
<Card
title="组织"
style={{ width: 300, flexShrink: 0 }}
extra={
<Space>
<Button
size="small"
icon={<PlusOutlined />}
onClick={() => {
setEditOrg(null);
orgForm.resetFields();
setOrgModalOpen(true);
}}
>
</Button>
{selectedOrg && (
<>
<Button
size="small"
icon={<EditOutlined />}
onClick={() => {
setEditOrg(selectedOrg);
orgForm.setFieldsValue({
name: selectedOrg.name,
code: selectedOrg.code,
sort_order: selectedOrg.sort_order,
});
setOrgModalOpen(true);
}}
/>
<Popconfirm
title="确定删除此组织?"
onConfirm={() => handleDeleteOrg(selectedOrg.id)}
>
<Button size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</>
)}
</Space>
}
>
{orgTree.length > 0 ? (
<Tree
showLine
defaultExpandAll
treeData={convertOrgTree(orgTree)}
onSelect={onSelectOrg}
selectedKeys={selectedOrg ? [selectedOrg.id] : []}
/>
) : (
<Empty description="暂无组织" />
)}
</Card>
{/* Middle: Department Tree */}
<Card
title={selectedOrg ? `${selectedOrg.name} - 部门` : '部门'}
style={{ width: 300, flexShrink: 0 }}
extra={
selectedOrg ? (
<Space>
<Button
size="small"
icon={<PlusOutlined />}
onClick={() => {
deptForm.resetFields();
setDeptModalOpen(true);
}}
>
</Button>
{selectedDept && (
<Popconfirm
title="确定删除此部门?"
onConfirm={() => handleDeleteDept(selectedDept.id)}
>
<Button size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
)}
</Space>
) : null
}
>
{selectedOrg ? (
deptTree.length > 0 ? (
<Tree
showLine
defaultExpandAll
treeData={convertDeptTree(deptTree)}
onSelect={onSelectDept}
selectedKeys={selectedDept ? [selectedDept.id] : []}
/>
) : (
<Empty description="暂无部门" />
)
) : (
<Empty description="请先选择组织" />
)}
</Card>
{/* Right: Positions */}
<Card
title={selectedDept ? `${selectedDept.name} - 岗位` : '岗位'}
style={{ flex: 1 }}
extra={
selectedDept ? (
<Button
size="small"
icon={<PlusOutlined />}
onClick={() => {
positionForm.resetFields();
setPositionModalOpen(true);
}}
>
</Button>
) : null
}
>
{selectedDept ? (
<Table
columns={positionColumns}
dataSource={positions}
rowKey="id"
size="small"
pagination={false}
/>
) : (
<Empty description="请先选择部门" />
)}
</Card>
</div>
{/* Org Modal */}
<Modal
title={editOrg ? '编辑组织' : selectedOrg ? `${selectedOrg.name} 下新建子组织` : '新建根组织'}
open={orgModalOpen}
onCancel={() => {
setOrgModalOpen(false);
setEditOrg(null);
}}
onOk={() => orgForm.submit()}
>
<Form form={orgForm} onFinish={handleCreateOrg} layout="vertical">
<Form.Item
name="name"
label="名称"
rules={[{ required: true, message: '请输入组织名称' }]}
>
<Input />
</Form.Item>
<Form.Item name="code" label="编码">
<Input />
</Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
{/* Dept Modal */}
<Modal
title={
selectedDept
? `${selectedDept.name} 下新建子部门`
: `${selectedOrg?.name} 下新建部门`
}
open={deptModalOpen}
onCancel={() => setDeptModalOpen(false)}
onOk={() => deptForm.submit()}
>
<Form form={deptForm} onFinish={handleCreateDept} layout="vertical">
<Form.Item
name="name"
label="名称"
rules={[{ required: true, message: '请输入部门名称' }]}
>
<Input />
</Form.Item>
<Form.Item name="code" label="编码">
<Input />
</Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
{/* Position Modal */}
<Modal
title={`${selectedDept?.name} 下新建岗位`}
open={positionModalOpen}
onCancel={() => setPositionModalOpen(false)}
onOk={() => positionForm.submit()}
>
<Form form={positionForm} onFinish={handleCreatePosition} layout="vertical">
<Form.Item
name="name"
label="岗位名称"
rules={[{ required: true, message: '请输入岗位名称' }]}
>
<Input />
</Form.Item>
<Form.Item name="code" label="编码">
<Input />
</Form.Item>
<Form.Item name="level" label="级别" initialValue={1}>
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
</div>
);
}
// --- Helpers ---
function findOrgInTree(
tree: OrganizationInfo[],
id: string,
): OrganizationInfo | null {
for (const item of tree) {
if (item.id === id) return item;
const found = findOrgInTree(item.children, id);
if (found) return found;
}
return null;
}
function findDeptInTree(
tree: DepartmentInfo[],
id: string,
): DepartmentInfo | null {
for (const item of tree) {
if (item.id === id) return item;
const found = findDeptInTree(item.children, id);
if (found) return found;
}
return null;
}

View 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>
);
}