import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
|
import {
|
useTranslate,
|
useNotify,
|
TextInput
|
} from 'react-admin';
|
import { Box, Button, Card, Stack, CardContent, Skeleton, TextField } 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 [parmas, setParmas] = useState({ condition: '' });
|
const toggledItemRef = useRef({});
|
const apiRef = useTreeViewApiRef();
|
|
useEffect(() => {
|
reload()
|
}, [role, originMenuIds])
|
|
const reload = () => {
|
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', parmas);
|
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);
|
}
|
|
|
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' })
|
}
|
|
})
|
}
|
|
const search = (e) => {
|
const value = e.target.value;
|
setParmas({
|
...parmas,
|
condition: value
|
})
|
reload()
|
|
}
|
|
return (
|
<>
|
<Card sx={{
|
ml: 1,
|
mr: 1,
|
height: 'calc(100vh - 140px)',
|
overflowY: 'auto'
|
}}>
|
<CardContent sx={{
|
overflow: 'auto',
|
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={{
|
display: 'flex',
|
justifyContent: 'space-between',
|
alignItems: 'center'
|
}}>
|
<TextField sx={{ width: '200px' }} label="搜索菜单" variant="outlined" value={parmas.condition} onChange={(e) => search(e)} />
|
<Button startIcon={<SaveIcon />} size="small" variant="contained" onClick={handleSave} sx={{ height: '40px' }}>
|
{translate('ra.action.save')}
|
</Button>
|
</Box>
|
|
<Box sx={{
|
minWidth: 290,
|
overflow: 'auto',
|
marginTop: '10px',
|
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>
|
|
</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;
|