| import React, { useState, useRef, useEffect, useMemo, useCallback } from "react"; | 
| import { | 
|     useTranslate, | 
|     useNotify, | 
| } from 'react-admin'; | 
| import { Box, Button, Card, Stack, CardContent, Skeleton } from '@mui/material'; | 
| import { SimpleTreeView, TreeItem, RichTreeView, useTreeViewApiRef } from '@mui/x-tree-view'; | 
| import SaveIcon from '@mui/icons-material/Save'; | 
| import request from '@/utils/request' | 
|   | 
| const DEFAULT_EXPAND_ALL = true; | 
|   | 
| const AssignPermissions = (props) => { | 
|     const { role, originMenuIds, setDrawerVal, closeCallback } = props; | 
|     const translate = useTranslate(); | 
|     const notify = useNotify(); | 
|   | 
|     const [loading, setLoading] = useState(false); | 
|     const [treeData, setTreeData] = useState([]); | 
|     const [selectedItems, setSelectedItems] = useState([]); | 
|     const [expandedItems, setExpandedItems] = useState([]); | 
|     const toggledItemRef = useRef({}); | 
|     const apiRef = useTreeViewApiRef(); | 
|   | 
|     useEffect(() => { | 
|         setSelectedItems(originMenuIds.map(item => item + "")); | 
|   | 
|         const transformTree = (treeData) => { | 
|             return treeData.map(data => { | 
|                 return { | 
|                     id: data.id + '', | 
|                     label: data.type === 0 ? translate(data.name || data.code) : data.name || data.code, | 
|                     type: data.type, | 
|                     children: (data.children && data.children.length > 0 ? transformTree(data.children) : null) | 
|                 } | 
|             }) | 
|         } | 
|         const http = async () => { | 
|             const res = await request.post('/menu' + '/tree', {}); | 
|             if (res?.data?.code === 200) { | 
|                 const transformData = transformTree(res.data.data); | 
|                 setTreeData(transformData); | 
|                 if (DEFAULT_EXPAND_ALL) { | 
|                     setExpandedItems(getAllItemsWithChildrenItemIds(transformData)); | 
|                 } | 
|             } else { | 
|                 notify(res.data.msg, { type: 'error' }); | 
|             } | 
|             setLoading(false); | 
|         } | 
|         setLoading(true); | 
|         setTimeout(() => { | 
|             http(); | 
|         }, 200); | 
|     }, [role, originMenuIds]) | 
|   | 
|   | 
|     const getAllItemItemIds = () => { | 
|         const ids = []; | 
|         const registerItemId = (item) => { | 
|             ids.push(item.id); | 
|             item.children?.forEach(registerItemId); | 
|         }; | 
|         treeData.forEach(registerItemId); | 
|         return ids; | 
|     }; | 
|   | 
|     const getAllItemsWithChildrenItemIds = (treeDataHandle) => { | 
|         const itemIds = []; | 
|         const registerItemId = (item) => { | 
|             if (item.children?.length) { | 
|                 itemIds.push(item.id); | 
|                 item.children.forEach(registerItemId); | 
|             } | 
|         }; | 
|         if (treeDataHandle) { | 
|             treeDataHandle.forEach(registerItemId); | 
|         } else { | 
|             treeData.forEach(registerItemId); | 
|         } | 
|         return itemIds; | 
|     }; | 
|   | 
|     const handleSelectedItemsChange = (event, newSelectedItems) => { | 
|         const itemsToSelect = []; | 
|         const itemsToUnSelect = {}; | 
|         Object.entries(toggledItemRef.current).forEach(([itemId, isSelected]) => { | 
|             const item = apiRef.current.getItem(itemId); | 
|             if (isSelected) { | 
|                 itemsToSelect.push(...getItemDescendantsIds(item)); | 
|   | 
|                 const parentIds = getParentIds(treeData, itemId); | 
|                 itemsToSelect.push(...parentIds); | 
|             } else { | 
|                 // 取消子节点 | 
|                 const treeNode = checkoutTreeNode(treeData, itemId); | 
|                 if (treeNode?.children && treeNode?.children.length > 0) { | 
|                     const allChildren = getItemDescendantsIds(treeNode); | 
|                     const childrenSet = new Set(allChildren); | 
|                     newSelectedItems = newSelectedItems.filter(item => !childrenSet.has(item)); | 
|                 } | 
|   | 
|                 // 取消父节点 | 
|                 const removeParentIfAllSiblingsDeselected = (itemId, newSelectedItems, treeData) => { | 
|                     let updatedSelectedItems = [...newSelectedItems]; | 
|                     let currentId = itemId; | 
|                     while (true) { | 
|                         const parentId = getParentId(treeData, currentId); | 
|                         if (!parentId) break; | 
|                         const siblings = getChildrenIds(treeData, parentId); | 
|                         const allSiblingsDeselected = siblings.every(siblingId => !updatedSelectedItems.includes(siblingId)); | 
|                         if (allSiblingsDeselected) { | 
|                             updatedSelectedItems = updatedSelectedItems.filter(id => id !== parentId); | 
|                             currentId = parentId; | 
|                         } else { | 
|                             break; | 
|                         } | 
|                     } | 
|                     return updatedSelectedItems; | 
|                 }; | 
|                 newSelectedItems = removeParentIfAllSiblingsDeselected(itemId, newSelectedItems, treeData); | 
|             } | 
|         }); | 
|   | 
|         const newSelectedItemsWithChildren = Array.from( | 
|             new Set( | 
|                 [...newSelectedItems, ...itemsToSelect].filter( | 
|                     (itemId) => !itemsToUnSelect[itemId], | 
|                 ), | 
|             ), | 
|         ); | 
|   | 
|         setSelectedItems(newSelectedItemsWithChildren); | 
|   | 
|         toggledItemRef.current = {}; | 
|     }; | 
|   | 
|     const handleSave = (event) => { | 
|         request.post('/role/scope/update', { | 
|             id: role.id, | 
|             menuIds: { | 
|                 checked: selectedItems, | 
|                 halfChecked: [] | 
|             } | 
|         }).then(res => { | 
|             if (res?.data.code === 200) { | 
|                 setDrawerVal(null); | 
|                 if (closeCallback) { | 
|                     closeCallback(); | 
|                 } | 
|                 notify(res.data.msg, { type: 'info', messageArgs: { _: res.data.msg } }) | 
|             } else { | 
|                 notify(res.data.msg, { type: 'error' }) | 
|             } | 
|   | 
|         }) | 
|     } | 
|   | 
|     return ( | 
|         <> | 
|             <Card sx={{ | 
|                 ml: 1, | 
|                 mr: 1, | 
|                 height: '620px' | 
|             }}> | 
|                 <CardContent sx={{ | 
|                     overflow: 'auto', | 
|                     height: '100%', | 
|                     display: 'flex', | 
|                     flexDirection: 'column', | 
|                     justifyContent: 'space-between' | 
|                 }}> | 
|                     <Box> | 
|                         <Box mb={1} sx={{ | 
|                             display: 'flex', | 
|                             justifyContent: 'space-between' | 
|                         }}> | 
|                             <Button onClick={() => { | 
|                                 setSelectedItems((oldSelected) => | 
|                                     oldSelected.length === 0 ? getAllItemItemIds() : [], | 
|                                 ); | 
|                             }}> | 
|                                 {selectedItems.length === 0 ? translate('ra.action.select_all') : translate('ra.action.unselect')} | 
|                             </Button> | 
|                             <Button onClick={() => { | 
|                                 setExpandedItems((oldExpanded) => | 
|                                     oldExpanded.length === 0 ? getAllItemsWithChildrenItemIds() : [], | 
|                                 ); | 
|                             }}> | 
|                                 {expandedItems.length === 0 ? translate('common.action.expandAll') : translate('common.action.collapseAll')} | 
|                             </Button> | 
|                         </Box> | 
|                         <Box sx={{ | 
|                             height: 480, | 
|                             minWidth: 290, | 
|                             overflow: 'auto', | 
|                             padding: 1, | 
|                             borderBottom: '1px solid background.paper', | 
|                             borderRadius: '4px', | 
|                             boxShadow: '0 1px 2px rgba(0, 0, 0, 0.2)', | 
|                             backgroundColor: 'background.paper', | 
|                         }}> | 
|                             {loading ? ( | 
|                                 <SkeletonBox /> | 
|                             ) : ( | 
|                                 <RichTreeView | 
|                                     multiSelect | 
|                                     checkboxSelection | 
|                                     apiRef={apiRef} | 
|                                     items={treeData} | 
|                                     selectedItems={selectedItems} | 
|                                     onSelectedItemsChange={handleSelectedItemsChange} | 
|                                     onItemSelectionToggle={(event, itemId, isSelected) => { | 
|                                         toggledItemRef.current[itemId] = isSelected; | 
|                                     }} | 
|   | 
|                                     expandedItems={expandedItems} | 
|                                     onExpandedItemsChange={(event, itemIds) => { | 
|                                         setExpandedItems(itemIds); | 
|                                     }} | 
|                                 /> | 
|                             )} | 
|   | 
|                         </Box> | 
|                     </Box> | 
|                     <Box sx={{ | 
|                         display: 'flex', | 
|                         justifyContent: 'flex-start' | 
|                     }}> | 
|                         <Button startIcon={<SaveIcon />} variant="contained" onClick={handleSave}> | 
|                             {translate('ra.action.save')} | 
|                         </Button> | 
|                     </Box> | 
|                 </CardContent> | 
|             </Card> | 
|         </> | 
|     ) | 
| } | 
|   | 
| const checkoutTreeNode = (treeData, targetId) => { | 
|     let result = null; | 
|     const checkout = (node) => { | 
|         if (node.id === targetId) { | 
|             result = node; | 
|             return true; | 
|         } else { | 
|             if (node.children) { | 
|                 for (const child of node.children) { | 
|                     if (checkout(child)) { | 
|                         return true; | 
|                     } | 
|                 } | 
|             } | 
|         } | 
|         return false; | 
|     }; | 
|     treeData.forEach(item => { | 
|         if (checkout(item)) { | 
|             return; | 
|         } | 
|     }); | 
|     return result; | 
| }; | 
|   | 
| const getItemDescendantsIds = (item) => { | 
|     const ids = []; | 
|     item.children?.forEach((child) => { | 
|         ids.push(child.id); | 
|         ids.push(...getItemDescendantsIds(child)); | 
|     }); | 
|     return ids; | 
| } | 
|   | 
| const getParentIds = (tree, targetId) => { | 
|     let parentIds = []; | 
|     const searchTree = (node, path = []) => { | 
|         if (node.id === targetId) { | 
|             parentIds = [...path]; | 
|             return true; | 
|         } | 
|         if (node.children) { | 
|             for (const child of node.children) { | 
|                 if (searchTree(child, [...path, node.id])) { | 
|                     return true; | 
|                 } | 
|             } | 
|         } | 
|         return false; | 
|     }; | 
|     tree.forEach(item => { | 
|         searchTree(item); | 
|     }) | 
|     return parentIds; | 
| }; | 
|   | 
| const getParentId = (tree, targetId) => { | 
|     let parentId = null; | 
|     const searchTree = (node) => { | 
|         if (node.children) { | 
|             for (const child of node.children) { | 
|                 if (child.id === targetId) { | 
|                     parentId = node.id; | 
|                     return true; | 
|                 } | 
|                 if (searchTree(child)) { | 
|                     return true; | 
|                 } | 
|             } | 
|         } | 
|         return false; | 
|     }; | 
|     tree.forEach(item => { | 
|         if (searchTree(item)) { | 
|             return parentId; | 
|         } | 
|     }); | 
|     return parentId; | 
| }; | 
|   | 
| const getChildrenIds = (tree, targetId) => { | 
|     let childrenIds = []; | 
|     const searchTree = (node) => { | 
|         if (node.id === targetId && node.children) { | 
|             childrenIds = node.children.map(child => child.id); | 
|         } else if (node.children) { | 
|             for (const child of node.children) { | 
|                 searchTree(child); | 
|             } | 
|         } | 
|     }; | 
|     tree.forEach(item => { | 
|         searchTree(item); | 
|     }); | 
|     return childrenIds; | 
| }; | 
|   | 
| const SkeletonBox = () => { | 
|     return ( | 
|         <Stack spacing={1}> | 
|             <Skeleton variant="rounded" width={200} height={20} /> | 
|             <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} /> | 
|             <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} /> | 
|             <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} /> | 
|             <Skeleton variant="rounded" width={200} height={20} /> | 
|             <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} /> | 
|             <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} /> | 
|             <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} /> | 
|             <Skeleton variant="rounded" width={200} height={20} /> | 
|             <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} /> | 
|             <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} /> | 
|             <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} /> | 
|             <Skeleton variant="rounded" width={200} height={20} /> | 
|             <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} /> | 
|             <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} /> | 
|             <Skeleton variant="rounded" width={200} height={20} style={{ marginLeft: '50px' }} /> | 
|         </Stack> | 
|     ) | 
| } | 
|   | 
| export default AssignPermissions; |