679 lines
20 KiB
TypeScript
Executable File
679 lines
20 KiB
TypeScript
Executable File
'use client';
|
||
|
||
import * as React from 'react';
|
||
import { useState, useMemo } from 'react';
|
||
import { Button } from '@nice/ui/components/button';
|
||
import { Input } from '@nice/ui/components/input';
|
||
import { Badge } from '@nice/ui/components/badge';
|
||
import { IconSearch, IconPlus, IconBuilding, IconBuildingBank, IconUsers } from '@tabler/icons-react';
|
||
import { UsersDataTable } from '@/components/organization/users-data-table';
|
||
import { UserDialog } from '@/components/organization/user-dialog';
|
||
|
||
import { PageInfo, SiteHeader } from '@/components/site-header';
|
||
import { useTRPC, useOrganization, useUser } from '@fenghuo/client';
|
||
import { useQuery } from '@tanstack/react-query';
|
||
import { toast } from '@nice/ui/components/sonner';
|
||
import { TreeSelector, type TreeNode, type TreeDragConfig } from '@/components/selector';
|
||
import { type OrganizationTreeNode, UserWithRelations, userWithRelationsSelect } from '@fenghuo/common';
|
||
import { type OrganizationDialogState, type UserDialogState } from '@/components/organization/types';
|
||
import { OrganizationDialog } from '@/components/organization/organization-dialog';
|
||
import { useSetPageInfo } from '@/components/providers/dashboard-provider';
|
||
|
||
|
||
|
||
export default function OrganizationPage() {
|
||
const trpc = useTRPC();
|
||
|
||
// 状态管理 - 将所有状态声明放在一起
|
||
const [selectedOrganization, setSelectedOrganization] = useState<TreeNode | null>(null);
|
||
const [organizationDialog, setOrganizationDialog] = useState<OrganizationDialogState>({
|
||
open: false,
|
||
});
|
||
const [userDialog, setUserDialog] = useState<UserDialogState>({
|
||
open: false,
|
||
});
|
||
const [organizationSearchQuery, setOrganizationSearchQuery] = useState('');
|
||
|
||
// 分页和筛选状态
|
||
const [pagination, setPagination] = useState({
|
||
page: 1,
|
||
pageSize: 10,
|
||
});
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
const [selectedRole, setSelectedRole] = useState('');
|
||
|
||
// 获取组织树数据(使用 tRPC)
|
||
const { data: rawOrganizationTree = [], isLoading: organizationsLoading } = useQuery({
|
||
...trpc.organization.getTree.queryOptions({
|
||
includeInactive: false,
|
||
include: {
|
||
terms: {
|
||
include: {
|
||
taxonomy: true,
|
||
},
|
||
},
|
||
},
|
||
}),
|
||
});
|
||
|
||
// 将 OrganizationTreeNode 转换为与 TreeSelector 兼容的 TreeNode
|
||
const organizationTree = useMemo(() => {
|
||
const convertNode = (node: OrganizationTreeNode): TreeNode => ({
|
||
...node,
|
||
slug: node.slug ?? undefined,
|
||
description: node.description ?? undefined,
|
||
parentId: node.parentId ?? undefined,
|
||
path: node.path ?? undefined,
|
||
deletedAt: node.deletedAt ?? undefined,
|
||
children: node.children ? node.children.map(convertNode) : [],
|
||
});
|
||
return rawOrganizationTree.map(convertNode);
|
||
}, [rawOrganizationTree]);
|
||
|
||
// 构建用户查询的 where 条件
|
||
const buildUserWhereCondition = useMemo(() => {
|
||
const conditions: any = {
|
||
deletedAt: null,
|
||
OR: [{ organization: { deletedAt: null } }, { organizationId: null }],
|
||
};
|
||
|
||
// 搜索条件 - 支持用户名搜索
|
||
if (searchQuery) {
|
||
conditions.username = {
|
||
contains: searchQuery,
|
||
mode: 'insensitive', // 忽略大小写
|
||
};
|
||
}
|
||
|
||
// 角色筛选
|
||
if (selectedRole) {
|
||
conditions.userRoles = {
|
||
some: {
|
||
role: {
|
||
id: selectedRole,
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
// 部门筛选 - 考虑选中的部门及其子部门
|
||
if (selectedOrganization) {
|
||
// 获取选中部门及其所有子部门的ID
|
||
const findOrganizationInTree = (depts: any[], targetId: string): any => {
|
||
for (const dept of depts) {
|
||
if (dept.id === targetId) {
|
||
return dept;
|
||
}
|
||
if (dept.children) {
|
||
const found = findOrganizationInTree(dept.children, targetId);
|
||
if (found) return found;
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const selectedDeptWithChildren = findOrganizationInTree(organizationTree, selectedOrganization.id);
|
||
if (selectedDeptWithChildren) {
|
||
const organizationIds = getAllOrganizationIds(selectedDeptWithChildren as OrganizationTreeNode);
|
||
conditions.organizationId = {
|
||
in: organizationIds,
|
||
};
|
||
} else {
|
||
conditions.organizationId = selectedOrganization.id;
|
||
}
|
||
}
|
||
|
||
return conditions;
|
||
}, [searchQuery, selectedRole, selectedOrganization, organizationTree]);
|
||
|
||
// 获取用户数据(使用服务端分页)
|
||
const { data: usersResponse, isLoading: usersLoading } = useQuery({
|
||
...trpc.user.findManyWithPagination.queryOptions({
|
||
page: pagination.page,
|
||
pageSize: pagination.pageSize,
|
||
where: buildUserWhereCondition,
|
||
select: userWithRelationsSelect,
|
||
orderBy: {
|
||
createdAt: 'desc',
|
||
},
|
||
}),
|
||
});
|
||
|
||
// 获取角色列表用于筛选选项
|
||
const { data: rolesResponse } = useQuery({
|
||
...trpc.role.findManyWithPagination.queryOptions({
|
||
page: 1,
|
||
pageSize: 100,
|
||
where: {
|
||
isActive: true,
|
||
},
|
||
}),
|
||
});
|
||
|
||
// 处理用户数据
|
||
const userData = useMemo(() => {
|
||
if (usersLoading || !usersResponse || !usersResponse.items) return [];
|
||
return usersResponse.items
|
||
}, [usersResponse, usersLoading]);
|
||
|
||
// 处理可用角色选项
|
||
const availableRoles = useMemo(() => {
|
||
if (!rolesResponse?.items) return [];
|
||
return rolesResponse.items.map((role) => ({
|
||
value: role.id,
|
||
label: role.name,
|
||
}));
|
||
}, [rolesResponse]);
|
||
|
||
// 分页信息
|
||
const paginationInfo = useMemo(() => {
|
||
if (!usersResponse) {
|
||
return {
|
||
totalPages: 0,
|
||
totalCount: 0,
|
||
hasNextPage: false,
|
||
hasPreviousPage: false,
|
||
};
|
||
}
|
||
return {
|
||
totalPages: usersResponse.totalPages,
|
||
totalCount: usersResponse.totalCount,
|
||
hasNextPage: usersResponse.hasNextPage,
|
||
hasPreviousPage: usersResponse.hasPreviousPage,
|
||
};
|
||
}, [usersResponse]);
|
||
|
||
// 过滤部门数据 - 修改为支持已有树形结构的搜索
|
||
const filteredOrganizations = React.useMemo(() => {
|
||
if (!organizationSearchQuery) return organizationTree;
|
||
|
||
const filterOrganizations = (depts: TreeNode[]): TreeNode[] => {
|
||
return depts.reduce<TreeNode[]>((acc, dept) => {
|
||
const matchesCurrent =
|
||
dept.name.toLowerCase().includes(organizationSearchQuery.toLowerCase()) ||
|
||
dept.description?.toLowerCase().includes(organizationSearchQuery.toLowerCase());
|
||
|
||
const filteredChildren = dept.children ? filterOrganizations(dept.children as TreeNode[]) : [];
|
||
|
||
// 如果当前部门匹配或有匹配的子部门,就包含这个部门
|
||
if (matchesCurrent || filteredChildren.length > 0) {
|
||
acc.push({
|
||
...dept,
|
||
children: filteredChildren,
|
||
});
|
||
}
|
||
|
||
return acc;
|
||
}, []);
|
||
};
|
||
return filterOrganizations(organizationTree);
|
||
}, [organizationTree, organizationSearchQuery]);
|
||
|
||
// 拖拽排序处理函数
|
||
const handleDragEnd = async (oldData: TreeNode[], newData: TreeNode[]) => {
|
||
// 提取排序变化
|
||
const changes = extractOrderChanges(oldData, newData);
|
||
|
||
// 如果有变化,调用后端API更新排序
|
||
if (changes.length > 0) {
|
||
try {
|
||
await organizationMutations.updateDeptOrder.mutateAsync(
|
||
changes.map((change) => ({ id: change.id, order: change.newOrder })),
|
||
);
|
||
toast.success('部门排序已更新');
|
||
} catch (error: any) {
|
||
console.error('排序更新失败:', error);
|
||
toast.error('排序更新失败: ' + (error.message || '未知错误'));
|
||
}
|
||
}
|
||
};
|
||
|
||
// 提取排序变化的辅助函数
|
||
const extractOrderChanges = (oldData: TreeNode[], newData: TreeNode[]): Array<{ id: string; newOrder: number }> => {
|
||
const changes: Array<{ id: string; newOrder: number }> = [];
|
||
|
||
const compareLevel = (oldNodes: TreeNode[], newNodes: TreeNode[], level = 0) => {
|
||
newNodes.forEach((newNode, index) => {
|
||
const oldNode = oldNodes.find((n) => n.id === newNode.id);
|
||
if (oldNode && oldNode.order !== newNode.order) {
|
||
changes.push({
|
||
id: newNode.id,
|
||
newOrder: newNode.order || index,
|
||
});
|
||
}
|
||
|
||
// 递归处理子节点
|
||
if (newNode.children && oldNode?.children) {
|
||
compareLevel(oldNode.children as TreeNode[], newNode.children as TreeNode[], level + 1);
|
||
}
|
||
});
|
||
};
|
||
|
||
compareLevel(oldData, newData);
|
||
return changes;
|
||
};
|
||
|
||
// 拖拽配置
|
||
const dragConfig: TreeDragConfig<TreeNode> = {
|
||
enabled: true,
|
||
showHandle: true,
|
||
onDragEnd: handleDragEnd,
|
||
onDragStart: (event) => {
|
||
console.log('开始拖拽:', event.active.id);
|
||
},
|
||
};
|
||
|
||
// 处理分页变化
|
||
const handlePaginationChange = (newPagination: { page: number; pageSize: number }) => {
|
||
setPagination(newPagination);
|
||
};
|
||
|
||
// 处理搜索变化
|
||
const handleSearchChange = (query: string) => {
|
||
setSearchQuery(query);
|
||
// 搜索时重置到第一页
|
||
setPagination((prev) => ({ ...prev, page: 1 }));
|
||
};
|
||
|
||
// 处理角色筛选变化
|
||
const handleRoleChange = (role: string) => {
|
||
setSelectedRole(role);
|
||
// 筛选时重置到第一页
|
||
setPagination((prev) => ({ ...prev, page: 1 }));
|
||
};
|
||
|
||
// 处理部门筛选变化 - 当选中部门时自动应用筛选
|
||
React.useEffect(() => {
|
||
// 当选中部门变化时,重置到第一页
|
||
setPagination((prev) => ({ ...prev, page: 1 }));
|
||
}, [selectedOrganization]);
|
||
|
||
// 处理部门选择 - 支持取消选中
|
||
const handleOrganizationSelect = (organization: TreeNode) => {
|
||
// 如果点击的是已选中的部门,则取消选中
|
||
if (selectedOrganization && selectedOrganization.id === organization.id) {
|
||
setSelectedOrganization(null);
|
||
} else {
|
||
// 否则选中新的部门
|
||
setSelectedOrganization(organization);
|
||
}
|
||
};
|
||
|
||
// 处理编辑部门
|
||
const handleEditOrganization = (organization: TreeNode) => {
|
||
setOrganizationDialog({
|
||
open: true,
|
||
organization: organization as OrganizationTreeNode,
|
||
mode: 'edit',
|
||
});
|
||
};
|
||
|
||
// 处理删除部门
|
||
const handleDeleteOrganization = (organization: TreeNode) => {
|
||
if (confirm(`确定要删除部门 "${organization.name}" 吗?这将同时删除该部门下的所有子部门。`)) {
|
||
organizationMutations.softDeleteByIds.mutate(
|
||
{ ids: [organization.id] },
|
||
{
|
||
onSuccess: () => {
|
||
toast.success('部门删除成功');
|
||
// 如果删除的是当前选中的部门,清空选择
|
||
if (selectedOrganization?.id === organization.id) {
|
||
setSelectedOrganization(null);
|
||
}
|
||
},
|
||
onError: (error) => {
|
||
toast.error('删除失败: ' + error.message);
|
||
},
|
||
},
|
||
);
|
||
}
|
||
};
|
||
|
||
// 处理添加子部门
|
||
const handleAddChildOrganization = (parentId: string) => {
|
||
setOrganizationDialog({
|
||
open: true,
|
||
parentId,
|
||
mode: 'addChild',
|
||
});
|
||
};
|
||
|
||
// 处理添加根部门
|
||
const handleAddRootOrganization = () => {
|
||
setOrganizationDialog({
|
||
open: true,
|
||
mode: 'add',
|
||
});
|
||
};
|
||
|
||
// 添加 organization mutations
|
||
const organizationMutations = useOrganization();
|
||
|
||
// 修改 handleSaveOrganization 方法的参数类型和实现
|
||
const handleSaveOrganization = (data: {
|
||
name: string;
|
||
slug: string;
|
||
description: string;
|
||
parentId?: string;
|
||
organizationTypeId?: string;
|
||
professionIds?: string[];
|
||
}) => {
|
||
// 构建术语连接数组
|
||
const termConnections: Array<{ id: string }> = [];
|
||
if (data.organizationTypeId) {
|
||
termConnections.push({ id: data.organizationTypeId });
|
||
}
|
||
if (data.professionIds && data.professionIds.length > 0) {
|
||
data.professionIds.forEach((professionId) => {
|
||
termConnections.push({ id: professionId });
|
||
});
|
||
}
|
||
|
||
// 构建要保存的数据
|
||
const createData = {
|
||
name: data.name,
|
||
slug: data.slug || null, // 使用用户输入的 slug
|
||
description: data.description || null,
|
||
parentId: data.parentId || null,
|
||
// 关联术语
|
||
terms:
|
||
termConnections.length > 0
|
||
? {
|
||
connect: termConnections,
|
||
}
|
||
: undefined,
|
||
};
|
||
|
||
// 检查是否为编辑模式
|
||
if (organizationDialog.organization && organizationDialog.mode === 'edit') {
|
||
// 更新部门时,需要先断开所有术语连接,再重新连接
|
||
const updateData = {
|
||
...createData,
|
||
terms: {
|
||
// 先断开所有现有的术语连接
|
||
set: termConnections, // 使用 set 替换所有关联
|
||
},
|
||
};
|
||
|
||
// 更新部门
|
||
organizationMutations.update.mutate(
|
||
{
|
||
where: { id: organizationDialog.organization.id },
|
||
data: updateData,
|
||
},
|
||
{
|
||
onSuccess: () => {
|
||
toast.success('部门更新成功');
|
||
setOrganizationDialog({ open: false });
|
||
},
|
||
onError: (error: any) => {
|
||
console.error('更新失败:', error);
|
||
let errorMessage = '更新失败';
|
||
if (error.message) {
|
||
if (
|
||
error.message.includes('already exists') ||
|
||
error.message.includes('same') ||
|
||
error.message.includes('unique') ||
|
||
error.message.includes('Unique constraint')
|
||
) {
|
||
errorMessage = '该部门名称或别名已存在,请使用其他名称';
|
||
} else {
|
||
errorMessage = error.message;
|
||
}
|
||
}
|
||
toast.error(errorMessage);
|
||
},
|
||
},
|
||
);
|
||
} else {
|
||
// 创建新部门
|
||
organizationMutations.create.mutate(
|
||
{
|
||
data: createData,
|
||
},
|
||
{
|
||
onSuccess: () => {
|
||
toast.success('部门创建成功');
|
||
setOrganizationDialog({ open: false });
|
||
},
|
||
onError: (error: any) => {
|
||
console.error('创建失败:', error);
|
||
let errorMessage = '创建失败';
|
||
if (error.message) {
|
||
if (
|
||
error.message.includes('already exists') ||
|
||
error.message.includes('same') ||
|
||
error.message.includes('unique') ||
|
||
error.message.includes('Unique constraint')
|
||
) {
|
||
errorMessage = '该部门名称或别名已存在,请使用其他名称';
|
||
} else {
|
||
errorMessage = error.message;
|
||
}
|
||
}
|
||
toast.error(errorMessage);
|
||
},
|
||
},
|
||
);
|
||
}
|
||
};
|
||
|
||
// 处理添加用户
|
||
const handleAddUser = () => {
|
||
setUserDialog({
|
||
open: true,
|
||
mode: 'add',
|
||
});
|
||
};
|
||
const { create: createUser } = useUser();
|
||
// 处理保存用户
|
||
const handleSaveUser = (data: {
|
||
username: string;
|
||
roleIds: string[];
|
||
organizationId: string;
|
||
password: string;
|
||
description?: string; // 添加可选的个人简介字段
|
||
}) => {
|
||
const { roleIds, ...others } = data
|
||
createUser.mutate(
|
||
{
|
||
data: {
|
||
...others,
|
||
roles: { connect: roleIds.map(id => ({ id })) }
|
||
}
|
||
}, // 由于我们传入完整的 data 对象,roleIds 字段会自动包含在内
|
||
{
|
||
onSuccess: () => {
|
||
toast.success('用户创建成功');
|
||
setUserDialog({ open: false });
|
||
},
|
||
onError: (error: any) => {
|
||
console.error('创建失败:', error);
|
||
let errorMessage = '创建失败';
|
||
if (error.message) {
|
||
if (
|
||
error.message.includes('already exists') ||
|
||
error.message.includes('same') ||
|
||
error.message.includes('unique') ||
|
||
error.message.includes('Unique constraint')
|
||
) {
|
||
errorMessage = '该用户名已存在,请使用其他名称';
|
||
} else {
|
||
errorMessage = error.message;
|
||
}
|
||
}
|
||
toast.error(errorMessage);
|
||
},
|
||
},
|
||
);
|
||
};
|
||
|
||
useSetPageInfo({
|
||
title: '组织管理',
|
||
subtitle: '管理组织结构,支持人员、组织的创建、编辑、删除以及查看等操作',
|
||
})
|
||
return (
|
||
<div className="flex flex-1 flex-col bg-gray-50/50 dark:bg-gray-900/50">
|
||
|
||
<div className="@container/main flex flex-1">
|
||
{/* 左侧:部门树 */}
|
||
<div className="w-96 border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||
{/* 标题区域 */}
|
||
<div className="border-b border-gray-200 dark:border-gray-700 p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||
<IconBuildingBank className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||
</div>
|
||
<div>
|
||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">组织结构</h2>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">选择一个组织来管理其用户</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Button size="sm" onClick={handleAddRootOrganization} className="shrink-0">
|
||
<IconPlus className="h-4 w-4 mr-1" />
|
||
新增
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 搜索框 */}
|
||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
||
<div className="relative">
|
||
<IconSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||
<Input
|
||
placeholder="搜索部门..."
|
||
value={organizationSearchQuery}
|
||
onChange={(e) => setOrganizationSearchQuery(e.target.value)}
|
||
className="pl-10"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 部门树 */}
|
||
<div className="flex-1 overflow-y-auto max-h-[670px]">
|
||
{organizationsLoading ? (
|
||
<div className="p-6 text-center text-gray-500">
|
||
<div className="animate-pulse">加载中...</div>
|
||
</div>
|
||
) : filteredOrganizations.length > 0 ? (
|
||
<div className="rounded-lg bg-white dark:bg-gray-800/50 p-2">
|
||
<TreeSelector
|
||
data={filteredOrganizations}
|
||
selectedNode={selectedOrganization}
|
||
onSelect={handleOrganizationSelect}
|
||
iconConfig={{
|
||
getIcon: (node, hasChildren, isExpanded) => {
|
||
if (node.level === 0) {
|
||
return IconBuildingBank;
|
||
}
|
||
return IconBuilding;
|
||
},
|
||
}}
|
||
actionConfig={{
|
||
onEdit: handleEditOrganization,
|
||
onDelete: handleDeleteOrganization,
|
||
onAddChild: handleAddChildOrganization,
|
||
editLabel: '编辑部门',
|
||
deleteLabel: '删除部门',
|
||
addChildLabel: '添加下级',
|
||
}}
|
||
displayConfig={{
|
||
showDescription: true,
|
||
selectedClassName: 'bg-primary/10 border border-primary/20',
|
||
hoverClassName: 'hover:bg-muted/50 border border-transparent',
|
||
}}
|
||
indentSize={20}
|
||
containerized={false}
|
||
className="space-y-1 bg-white"
|
||
dragConfig={dragConfig}
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div className="p-6 text-center">
|
||
<div className="text-gray-400 mb-2">{organizationSearchQuery ? '没有找到匹配的部门' : '暂无部门'}</div>
|
||
{!organizationSearchQuery && <div className="text-sm text-gray-500">点击上方按钮创建第一个组织</div>}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 右侧:用户管理 */}
|
||
<div className="flex-1 bg-white dark:bg-gray-800">
|
||
{/* 标题区域 */}
|
||
<div className="border-b border-gray-200 dark:border-gray-700 p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
|
||
<IconUsers className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">用户管理</h2>
|
||
{selectedOrganization && (
|
||
<Badge variant="outline" className="text-xs">
|
||
{selectedOrganization.name}
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||
{selectedOrganization
|
||
? `显示 ${selectedOrganization.name} 及其下属部门的用户`
|
||
: `共 ${paginationInfo.totalCount} 名用户`}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Button size="sm" onClick={handleAddUser} className="shrink-0">
|
||
<IconPlus className="h-4 w-4 mr-1" />
|
||
新增用户
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 用户内容区域 */}
|
||
<div className="flex-1 p-6">
|
||
<UsersDataTable
|
||
data={userData as UserWithRelations[]}
|
||
pagination={pagination}
|
||
paginationInfo={paginationInfo}
|
||
onPaginationChange={handlePaginationChange}
|
||
searchQuery={searchQuery}
|
||
onSearchChange={handleSearchChange}
|
||
selectedRole={selectedRole}
|
||
onRoleChange={handleRoleChange}
|
||
selectedOrganizationId={selectedOrganization?.id || ''}
|
||
onOrganizationChange={() => { }} // 部门筛选通过左侧树形选择处理
|
||
isLoading={usersLoading}
|
||
availableRoles={availableRoles}
|
||
onAddUser={handleAddUser}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 部门编辑对话框 */}
|
||
<OrganizationDialog
|
||
dialog={organizationDialog}
|
||
onClose={() => setOrganizationDialog({ open: false })}
|
||
onSave={handleSaveOrganization}
|
||
/>
|
||
|
||
{/* 用户编辑对话框 */}
|
||
<UserDialog dialog={userDialog} onClose={() => setUserDialog({ open: false })} onSave={handleSaveUser} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 递归获取部门及其所有子部门的ID
|
||
function getAllOrganizationIds(organization: OrganizationTreeNode): string[] {
|
||
const ids = [organization.id];
|
||
if (organization.children) {
|
||
organization.children.forEach((child) => {
|
||
ids.push(...getAllOrganizationIds(child));
|
||
});
|
||
}
|
||
return ids;
|
||
}
|