Kaynağa Gözat

feat: 优化拖拽

xieyonghong 3 yıl önce
ebeveyn
işleme
06520c2a71

+ 14 - 0
src/pages/system/Menu/Setting/DragItem.css

@@ -0,0 +1,14 @@
+.menu-setting-drag-tree .ant-tree-treenode.ant-tree-treenode-draggable {
+  width: 100%;
+}
+.menu-setting-drag-tree .ant-tree-node-content-wrapper {
+  flex: 1;
+}
+.menu-setting-drag-tree .tree-drag-item {
+  display: flex;
+  align-items: center;
+}
+.menu-setting-drag-tree .tree-drag-item > span:first-child {
+  flex: 1;
+  padding-right: 4px;
+}

+ 20 - 0
src/pages/system/Menu/Setting/DragItem.less

@@ -0,0 +1,20 @@
+
+.menu-setting-drag-tree {
+  .ant-tree-treenode.ant-tree-treenode-draggable {
+    width: 100%;
+  }
+
+  .ant-tree-node-content-wrapper {
+    flex: 1;
+  }
+
+  .tree-drag-item {
+    display: flex;
+    align-items: center;
+
+    & >span:first-child {
+      flex: 1;
+      padding-right: 4px;
+    }
+  }
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 1397 - 42
src/pages/system/Menu/Setting/baseMenu.ts


+ 82 - 27
src/pages/system/Menu/Setting/dragItem.tsx

@@ -1,43 +1,98 @@
-import { useRef } from 'react';
-import { useDrag, useDrop } from 'react-dnd';
+import {useDrag, useDrop} from 'react-dnd';
+import {useEffect, useRef, useState} from "react";
+import { CloseOutlined } from '@ant-design/icons';
+import classNames from 'classnames';
+import { DragType } from './tree';
+import { Popconfirm, Tooltip } from 'antd';
 
 interface DragItemProps {
   data: any;
   type: string;
+  canDrag: boolean;
+  isSearch?: boolean;
+  onDrop?: (item: any,dropIndex: string, type?: 'source' | 'menu') => void
+  removeDragItem?: (id: string | number) => void
 }
 
 const DragItem = (props: DragItemProps) => {
-  const ref = useRef<HTMLDivElement>(null);
-  const [{ isOver, dropClassName }, drop] = useDrop({
-    accept: props.type,
+
+  const ref = useRef<HTMLDivElement>(null)
+  const [isDrag, setIsDrag] = useState(props.type === 'source')
+
+  useEffect(() => {
+    setIsDrag(props.type === 'source' && props.canDrag)
+  }, [props.canDrag])
+
+  const [, drop] = useDrop(() => ({
+    accept: DragType,
+    drop(item: any) {
+      props.onDrop?.(item.data, props.data.code, item.type)
+      return undefined
+    },
     collect: (monitor) => {
-      console.log(monitor.getItem());
-      // const { index: dragIndex } = monitor.getItem() || {};
-      // if (dragIndex === index) {
-      //   return {};
-      // }
+      const { index } = monitor.getItem() || {};
+      if (index === props.data.code) {
+        return {}
+      }
       return {
         isOver: monitor.isOver(),
-        dropClassName: 'dropping',
-      };
-    },
-    drop: (item: { index: React.Key }) => {
-      console.log(item);
-      // moveNode(item.index, index);
+        canDrop: monitor.canDrop(),
+      }
     },
-  });
-  const [, drag] = useDrag({
-    type: props.type,
-    item: { index: props.data.id },
-    collect: (monitor) => ({
-      isDragging: monitor.isDragging(),
-    }),
-  });
-  drop(drag(ref));
+  }))
+
+  const [, drag] = useDrag(() => (
+    {
+      type: DragType,
+      item: {
+        index: props.data.code,
+        data: props.data,
+        type: props.type
+      },
+      collect: (monitor) => {
+        return {
+          isDragging: monitor.isDragging(),
+        }
+      },
+      canDrag: isDrag
+    }
+  ), [isDrag, props.data]);
+
+
+  drag(drop(ref))
+
+  const style = props.type === 'source' ? isDrag ? {
+    border: '1px solid #E0E0E0',
+    padding: '0px 4px',
+    backgroundColor: '#F6F6F6',
+    borderRadius: 4,
+    color: '#2F54EB',
+    width: 'fit-content'
+  } : {
+    color: '#666666',
+    cursor: 'not-allowed',
+    width: 'fit-content'
+  } : {}
 
   return (
-    <div ref={ref} className={isOver ? dropClassName : ''}>
-      {props.data.name}
+    <div ref={ref} style={style} className={classNames({'tree-drag-item': props.type === 'menu'})}>
+      <span style={props.isSearch ? { color: '#f50' }: {}}>
+       {props.data.name}
+      </span>
+      {
+        props.type === 'menu' && 
+        <Popconfirm
+          title='确认删除?'
+          onConfirm={(e: any) => {
+            e?.stopPropagation()
+            props.removeDragItem?.(props.data.code)
+          }}
+        >
+          <Tooltip title='删除' >
+            <CloseOutlined />
+          </Tooltip>
+        </Popconfirm>
+      }
     </div>
   );
 };

+ 0 - 0
src/pages/system/Menu/Setting/index.css


+ 2 - 1
src/pages/system/Menu/Setting/index.less

@@ -34,7 +34,8 @@
         font-weight: 400;
         font-size: 16px;
         background-color: #f3f4f4;
-
+        align-items: center;
+        
         > span {
           margin-left: 16px;
         }

+ 199 - 117
src/pages/system/Menu/Setting/index.tsx

@@ -1,5 +1,5 @@
-import { PageContainer } from '@ant-design/pro-layout';
-import { useDomFullHeight } from '@/hooks';
+import {PageContainer} from '@ant-design/pro-layout';
+import {useDomFullHeight} from '@/hooks';
 import {
   ExclamationCircleOutlined,
   QuestionCircleOutlined,
@@ -7,163 +7,245 @@ import {
 } from '@ant-design/icons';
 import Tree from './tree';
 import './index.less';
-import { Button, Tooltip } from 'antd';
+import {Button, Tooltip} from 'antd';
 import BaseTreeData from './baseMenu';
-import { useEffect, useState } from 'react';
-import { HTML5Backend } from 'react-dnd-html5-backend';
-import { DndProvider } from 'react-dnd';
-
-export default () => {
-  const { minHeight } = useDomFullHeight(`.menu-setting-warp`);
-  const [menuData] = useState<any[]>([]);
-  const [baseMenu, setBaseMenu] = useState<any[]>([]);
-
-  // const removeItem = (data: any[], id: string): any[] => {
-  //   return data.filter(item => {
-  //     if (item.id === id) {
-  //       return false
-  //     }
-  //
-  //     if (item.children) {
-  //       item.children = removeItem(item.children, id)
-  //     }
-  //     return true
-  //   })
-  // }
-
-  // const findItem = (data: any[], id: string) => {
-  //   let object = null;
-  //   data.some((item) => {
-  //     if (item.id === id) {
-  //       object = item;
-  //       return true;
-  //     }
-  //
-  //     if (item.children) {
-  //       object = findItem(item.children, id);
-  //       return !!object;
-  //     }
-  //
-  //     return false;
-  //   });
-  //   return object;
-  // };
-
-  // const finedIndex = (data: any[], id: string): { index: number; menus: any[] } => {
-  //   let object = {
-  //     index: data.length,
-  //     menus: data,
-  //   };
-  //   data.some((item, index) => {
-  //     if (item.id === id) {
-  //       object = {
-  //         index,
-  //         menus: data,
-  //       };
-  //       return true;
-  //     }
-  //
-  //     if (item.children) {
-  //       object = finedIndex(item.children, id);
-  //       return !!object;
-  //     }
-  //
-  //     return false;
-  //   });
-  //
-  //   return object;
-  // };
-
-  // const onDragEnd = useCallback(
-  //   (result: any) => {
-  //     console.log(result);
-  //     if (result.source.droppableId.includes('source')) {
-  //       if (result.combine && result.combine.droppableId.includes('menu')) {
-  //         const sourceIndex = result.source.index.replace(/(source|menu)&/, '');
-  //         const draggableIdIndex = result.combine?.draggableId.replace(/(source|menu)&/, '');
-  //         const sourceItem = findItem(baseMenu, sourceIndex);
-  //         const newMenus = [...menuData];
-  //         const { index, menus } = finedIndex(newMenus, draggableIdIndex);
-  //         console.log(index, menus);
-  //         menus.splice(index + 1, 0, sourceItem);
-  //         console.log(newMenus);
-  //         setMenuData([...newMenus]);
-  //       } else if (result.destination && result.destination.droppableId.includes('menu')) {
-  //         const sourceIndex = result.source.index.replace(/(source|menu)&/, '');
-  //         const sourceItem = findItem(baseMenu, sourceIndex);
-  //         const newMenus = [...menuData];
-  //         if (sourceItem) {
-  //           if (newMenus.length) {
-  //             const destinationIndex = result.destination?.index.replace(/(source|menu)&/, '');
-  //             // 获取右侧menu的位置
-  //             const { index, menus } = finedIndex(newMenus, destinationIndex);
-  //             console.log(index, menus);
-  //             menus.splice(index + 1, 0, sourceItem);
-  //           } else {
-  //             newMenus.push(sourceItem);
-  //           }
-  //           console.log(newMenus);
-  //           setMenuData([...newMenus]);
-  //         }
-  //       }
-  //     }
-  //   },
-  //   [menuData, baseMenu],
-  // );
+import { useEffect, useState} from 'react';
+import {HTML5Backend} from 'react-dnd-html5-backend';
+import {DndProvider} from 'react-dnd';
+import {cloneDeep} from 'lodash'
+import { observable } from '@formily/reactive';
+import { Observer, observer } from '@formily/react'
+import { service } from '../index'
+import type {TreeProps, DataNode} from 'antd/es/tree'
+
+type MenuSettingModelType = {
+  menuData: any[]
+  notDragKeys: (string | number)[]
+}
+
+export const MenuSettingModel = observable<MenuSettingModelType>({
+  menuData: [],
+  notDragKeys: []
+})
+
+export default observer(() => {
+  const {minHeight} = useDomFullHeight(`.menu-setting-warp`);
+  const [baseMenu, setBaseMenu] = useState<any[]>(BaseTreeData);
+  const [loading, setLoading] = useState(false)
+
+  const finedObject = (data: any[], code: string | number, callback: (index: number, arr: any[], item: any) => void) => {
+
+    data.forEach((item, index) => {
+      if (item.code === code) {
+        return callback(index, data, item)
+      }
+
+      if (item.children) {
+        finedObject(item.children, code, callback);
+      }
+    });
+  };
+
+  const onTreeDrop: TreeProps['onDrop'] = (info) => {
+    const dropKey = info.node.key;
+    const dragKey = info.dragNode.key;
+    const dropPos = info.node.pos.split('-');
+    const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
+
+    const data = [...MenuSettingModel.menuData];
+    let dragObj: DataNode & {parentId: string};
+
+    finedObject(data, dragKey, (index, arr, item) => {
+      arr.splice(index, 1);
+      dragObj = item;
+      dragObj.parentId = '';
+    });
+
+    if (!info.dropToGap) {
+      finedObject(data, dropKey, (_i, _arr, item) => {
+        item.children = item.children || [];
+        dragObj.parentId = item.id
+        
+        item.children.unshift(dragObj);
+      });
+    } else if (
+      ((info.node as any).props.children || []).length > 0 && 
+      (info.node as any).props.expanded && 
+      dropPosition === 1 
+    ) {
+      finedObject(data, dropKey, (_i, _arr, item) => {
+        item.children = item.children || [];
+        dragObj.parentId = item.id 
+      
+        item.children.unshift(dragObj);
+      });
+    } else {
+      let ar: DataNode[] = [];
+      let i: number;
+      finedObject(data, dropKey, (index, arr, item) => {
+        ar = arr;
+        i = index;
+        dragObj.parentId = item.parentId;
+      });
+      if (dropPosition === -1) {
+        ar.splice(i!, 0, dragObj!);
+      } else {
+        ar.splice(i! + 1, 0, dragObj!);
+      }
+    }
+    MenuSettingModel.menuData = [...data]
+  }
+
+  const getKeys = (data: any[]): (string|number)[] => {
+    return data.reduce((pre: (string|number)[] , next: any) => {
+      const childrenKeys = next.children ? getKeys(next.children) : []
+      return [...pre, next.code, ...childrenKeys]
+    }, [])
+  }
+
+  const filterItem = (data: any[], dragKeys: (string | number)[]) => {
+    return data.filter(item => {
+      if (dragKeys.includes(item.code)) {
+        return false
+      } else if (item.children) {
+        item.children = filterItem(item.children, dragKeys)
+      }
+      return true
+    })
+  }
+
+  const onDrop = (item: any, dropIndex: string, type?: string) => {
+      const newItem = cloneDeep(item);
+      
+      if (dropIndex) { // 切换层级或者挪动
+        // const newMenus = cloneDeep(menuData);
+        if (type === 'source') {
+          finedObject(MenuSettingModel.menuData, dropIndex, (index, arr) => {
+            arr.splice(index + 1, 0, newItem);
+          });
+          MenuSettingModel.menuData = [...MenuSettingModel.menuData];
+        }
+      } else { // 新增
+        newItem.parentId = ''
+        // 过滤掉内部已选择的节点
+        if (newItem.children && MenuSettingModel.notDragKeys.length) {
+          newItem.children = filterItem(newItem.children, MenuSettingModel.notDragKeys)
+        }
+        MenuSettingModel.menuData.push(newItem)
+        MenuSettingModel.notDragKeys = getKeys(MenuSettingModel.menuData)
+      }
+    }
+
+  const removeDragItem = (id: string | number) => {
+    finedObject(MenuSettingModel.menuData, id, (index, arr) => {
+      arr.splice(index, 1);
+    });
+    MenuSettingModel.menuData = [...MenuSettingModel.menuData]
+    MenuSettingModel.notDragKeys = getKeys(MenuSettingModel.menuData)
+  }
+
+  const getSystemMenu = () => {
+    service.queryMenuThree({ paging: false }).then(res => {
+      if (res.status === 200) {
+        MenuSettingModel.menuData = [...res.result]
+        MenuSettingModel.notDragKeys = getKeys(res.result)
+      }
+    })
+  }
 
   useEffect(() => {
+    getSystemMenu()
     setBaseMenu(BaseTreeData);
   }, []);
 
+  const updateMenu = () => {
+    setLoading(true)
+    service.updateMenus(MenuSettingModel.menuData).then(res => {
+      setLoading(false)
+      if (res.status === 200) {
+
+      }
+    })
+  }
+
   return (
     <PageContainer>
-      <div className={'menu-setting-warp'} style={{ minHeight }}>
+      <div className={'menu-setting-warp'} style={{minHeight}}>
         <div className={'menu-setting-tip'}>
-          <ExclamationCircleOutlined />
-          基于系统源代码中的菜单数据,配置系统菜单。
+          <ExclamationCircleOutlined/>
+          <span style={{paddingLeft: 12}}>
+            基于系统源代码中的菜单数据,配置系统菜单。
+          </span>
         </div>
         <div className={'menu-tree-content'}>
           <DndProvider backend={HTML5Backend}>
             <div className={'menu-tree left-tree'}>
-              <div className={'menu-tree-title'}>
+              <div className={'menu-tree-title'} style={{padding: '10px 16px'}}>
                 <div>
+                  <span style={{paddingRight: 12}}>
                   源菜单
+                  </span>
                   <Tooltip title={'根据系统代码自动读取的菜单数据'}>
-                    <QuestionCircleOutlined />
+                    <QuestionCircleOutlined/>
                   </Tooltip>
                 </div>
-                <Button type={'primary'} ghost>
+                <Button type={'primary'} ghost onClick={() => {
+                  MenuSettingModel.menuData = cloneDeep(baseMenu)
+                }}>
                   一键拷贝
                 </Button>
               </div>
-              <Tree treeData={baseMenu} droppableId={'source'} />
+              <Observer>
+                {() => (
+                  <Tree
+                    onDrop={onDrop}
+                    treeData={baseMenu}
+                    droppableId={'source'}
+                    notDragKeys={MenuSettingModel.notDragKeys}
+                  />
+                )}
+              </Observer>
             </div>
-            <div style={{ display: 'flex', alignItems: 'center' }}>
+            <div style={{display: 'flex', alignItems: 'center'}}>
               <div className={'menu-tree-drag-btn'}>
                 请拖动至右侧
-                <RightOutlined />
+                <RightOutlined/>
               </div>
             </div>
 
             <div className={'menu-tree right-tree'}>
               <div className={'menu-tree-title'}>
                 <div>
+                  <span style={{paddingRight: 12}}>
                   系统菜单
+                  </span>
                   <Tooltip title={'菜单管理页面配置的菜单数据'}>
-                    <QuestionCircleOutlined />
+                    <QuestionCircleOutlined/>
                   </Tooltip>
                 </div>
               </div>
-              <Tree treeData={menuData} droppableId={'menu'} />
+              <Observer>
+                {() => (
+                  <>
+                  <Tree
+                    onTreeDrop={onTreeDrop}
+                    onDrop={onDrop}
+                    treeData={MenuSettingModel.menuData}
+                    droppableId={'menu'}
+                    removeDragItem={removeDragItem}
+                  />
+                  </>
+                )}
+              </Observer>
             </div>
           </DndProvider>
         </div>
         <div>
-          <Button type={'primary'} style={{ marginTop: 24 }}>
+          <Button type={'primary'} style={{ marginTop: 24 }} onClick={updateMenu} loading={loading}>
             保存
           </Button>
         </div>
       </div>
     </PageContainer>
   );
-};
+});

+ 120 - 8
src/pages/system/Menu/Setting/tree.tsx

@@ -1,41 +1,153 @@
 import { Input, Tree } from 'antd';
 import { SearchOutlined } from '@ant-design/icons';
 import DragItem from '@/pages/system/Menu/Setting/dragItem';
+import { useDrop } from 'react-dnd';
+import {useEffect, useState} from 'react'
+import type {TreeProps} from 'antd'
+import { cloneDeep, debounce } from 'lodash'
+import './DragItem.less'
 
 interface TreeBodyProps {
   treeData: any[];
   droppableId: string;
+  notDragKeys?: (string| number)[]
+  onDrop?: (item: any,dropIndex: string, type?: string) => void
+  onTreeDrop?: TreeProps['onDrop']
+  removeDragItem?: (id: string | number) => void
+  className?: string
 }
 
+export const DragType = 'DragBox'
+
 const { TreeNode } = Tree;
 
 export default (props: TreeBodyProps) => {
+  const [newData, setNewData] = useState(props.treeData)
+  const [searchKeys, setSearchKeys] = useState<(string| number)[]>([])
+  const [expandedKeys, setExpandedKeys] = useState<(string| number)[]>([])
+  
+  useEffect(() => {
+    setNewData(cloneDeep(props.treeData))
+  }, [props.treeData])
+
+  const [, drop] = useDrop(() => ({
+    accept: DragType,
+    drop(item: any, monitor) {
+      const result = monitor.getDropResult()
+      if (!result && props.onDrop) {
+        props.onDrop(item.data, '')
+      }
+      return undefined
+    },
+    collect: (monitor) => {
+      return {
+        isOver: monitor.isOver(),
+        canDrop: monitor.canDrop(),
+        draggingColor: monitor.getItemType(),
+      }
+    },
+  }))
+
   const createTreeNode = (data: any[], type: string): React.ReactNode => {
     return data.map((item: any) => {
+      const isCanDrag = !props.notDragKeys?.includes(item.code)
+
       if (item.children) {
         return (
-          <TreeNode title={<DragItem data={item} type={type} />}>
+          <TreeNode
+          selectable={false}
+          key={item.code}
+          title={
+          <DragItem 
+            onDrop={props.onDrop}
+            data={item}
+            type={type}
+            canDrag={isCanDrag}
+            removeDragItem={props.removeDragItem}
+            isSearch={searchKeys.includes(item.code)}
+            />
+           }>
             {createTreeNode(item.children, type)}
           </TreeNode>
         );
       }
-      return <TreeNode title={<DragItem data={item} type={type} />}></TreeNode>;
+      return <TreeNode
+      selectable={false}
+      key={item.code}
+      title={
+      <DragItem 
+        onDrop={props.onDrop}
+        data={item}
+        type={type}
+        canDrag={isCanDrag}
+        removeDragItem={props.removeDragItem}
+        isSearch={searchKeys.includes(item.code)}
+        />
+     } />;
     });
-  };
+  }
+
+  const findAllItem = (data: any[], value: string): string[] => {
+    return data.reduce((pre, next) => {
+      const childrenKeys = next.children ? findAllItem(next.children, value) : []
+      return next.name.includes(value) ? [...pre, next.code, ...childrenKeys] : [...pre, ...childrenKeys]
+    }, [])
+
+  }
+
+  const searchValue = (e: any) => {
+    const value = e.target.value
+
+    if (value) {
+      const sKeys = findAllItem(props.treeData, value)
+      setSearchKeys(sKeys)
+      setExpandedKeys(sKeys)
+    } else {
+      setSearchKeys([])
+    }
+  }
+
 
   return (
     <div className={'tree-content'}>
       <div style={{ width: '75%' }}>
         <Input
+          allowClear
           prefix={<SearchOutlined style={{ color: '#B3B3B3' }} />}
           placeholder={'请输入菜单名称'}
+          onChange={debounce(searchValue, 500)}
         />
       </div>
-      <div className={'tree-body'}>
-        <Tree draggable={props.droppableId === 'menu'}>
-          {createTreeNode(props.treeData, props.droppableId)}
-        </Tree>
-      </div>
+      {
+        props.droppableId === 'source' ?
+        <div className={'tree-body'}>
+          <Tree
+            expandedKeys={expandedKeys}
+            onExpand={(_expandedKeys) => {
+              setExpandedKeys(_expandedKeys)
+            }}
+          >
+            {createTreeNode(newData, props.droppableId)}
+          </Tree>
+        </div>
+          :
+        <div className={'tree-body'} ref={drop} >
+          <Tree
+            expandedKeys={expandedKeys}
+            onExpand={(_expandedKeys) => {
+              setExpandedKeys(_expandedKeys)
+            }}
+            draggable={{
+              icon: false
+            }}
+            onDrop={props.onTreeDrop}
+            className='menu-setting-drag-tree'
+          >
+            {createTreeNode(props.treeData, props.droppableId)}
+          </Tree>
+        </div>
+
+      }
     </div>
   );
 };

+ 3 - 0
src/pages/system/Menu/service.ts

@@ -27,6 +27,9 @@ class Service extends BaseService<MenuItem> {
 
   // 资产类型
   queryAssetsType = () => request(`${SystemConst.API_BASE}/asset/types`, { method: 'GET' });
+
+  // 更新全部菜单
+  updateMenus = (data: any) => request(`${this.uri}`, {method: 'PATCH', data})
 }
 
 export default Service;