Browse Source

feat: 用户增加菜单权限分配

xyh 2 years ago
parent
commit
91caf9bd35

+ 10 - 0
packages/app/src/apis/user.ts

@@ -2,6 +2,7 @@ import {
   AddUserParams,
   BaseListResult,
   BaseResult,
+  EditUserMenuParams,
   EditUserParams,
   EditUserPasswordParams,
   GetUserListParams,
@@ -78,6 +79,15 @@ export function editUser(data: EditUserParams): BaseResult {
   });
 }
 
+/** 修改用户菜单权限 */
+export function editUserMenu(data: EditUserMenuParams): BaseResult {
+  return request({
+    method: 'PUT',
+    data,
+    url: `${BASE_URL}/updateUser`,
+  });
+}
+
 /** 删除用户 */
 export function deleteUser(id: string): BaseResult {
   return request({

+ 8 - 0
packages/app/src/models/request/user.ts

@@ -31,6 +31,7 @@ export type AddUserParams = Omit<
   | 'code'
   | 'modifyUser'
   | 'modifyTime'
+  | 'menu'
 >;
 
 /** 修改用户 */
@@ -43,8 +44,15 @@ export type EditUserParams = Omit<
   | 'password'
   | 'modifyUser'
   | 'modifyTime'
+  | 'menu'
 >;
 
+/** 修改用户菜单权限 */
+export type EditUserMenuParams = {
+  id: string;
+  menu: string;
+};
+
 /** 修改密码 */
 export type EditUserPasswordParams = {
   /** 用户id */

+ 2 - 0
packages/app/src/models/response/user.ts

@@ -41,4 +41,6 @@ export type UserListData = {
   roleId: string;
   /** 创建时间 */
   createTime: string;
+  /** 用户菜单权限 */
+  menu: string;
 } & ModifyData;

+ 16 - 5
packages/app/src/pages/user/table/hooks.tsx

@@ -5,7 +5,7 @@ import {UserListData} from '@models';
 import {ColumnsType} from 'antd/es/table';
 import {Button, Modal, message} from 'antd';
 import {
-  LARGE_TABLE_WIDTH,
+  GREAT_TABLE_WIDTH,
   MIDDLE_TABLE_WIDTH,
   NORMAL_TABLE_WIDTH,
   SMALL_TABLE_WIDTH,
@@ -81,6 +81,10 @@ export function useHandle(onFetch: () => void) {
     label: '用户',
   });
   const [{visible, editId}, {onClose, onEdit, onAdd}] = useTableModalEvent();
+  const [
+    {visible: menuVisible, editId: menuUserId},
+    {onClose: onMenuClose, onEdit: onMenuEdit},
+  ] = useTableModalEvent();
   const [isReseting, onResetClick] = useResetPassword();
 
   const columns: ColumnsType<UserListData> = [
@@ -90,7 +94,7 @@ export function useHandle(onFetch: () => void) {
       dataIndex: 'id',
       key: 'id',
       fixed: 'right',
-      width: LARGE_TABLE_WIDTH,
+      width: GREAT_TABLE_WIDTH,
       render(_, {id, realName, userName}) {
         return (
           <>
@@ -102,7 +106,14 @@ export function useHandle(onFetch: () => void) {
             >
               修改
             </Button>
-
+            <Button
+              type='text'
+              className='ant-text-btn-color-primary'
+              disabled={String(id) === pendingId}
+              onClick={onMenuEdit(String(id))}
+            >
+              菜单权限
+            </Button>
             <Button
               type='text'
               className='ant-text-btn-color-primary'
@@ -127,7 +138,7 @@ export function useHandle(onFetch: () => void) {
   ];
 
   return [
-    {columns, visible, editId},
-    {onAdd, onClose},
+    {columns, visible, editId, menuVisible, menuUserId},
+    {onAdd, onClose, onMenuClose},
   ] as const;
 }

+ 12 - 1
packages/app/src/pages/user/table/index.tsx

@@ -10,6 +10,7 @@ import {
   useTableExportEvent,
 } from '@hooks';
 import {exportUser, getUserList} from '@apis';
+import TreeModal from './tree-modal';
 
 const TableList: FC = function () {
   const params = useContextSection(context, state => state[0]);
@@ -19,7 +20,10 @@ const TableList: FC = function () {
     pageContext,
     searchContext,
   });
-  const [{columns, visible, editId}, {onAdd, onClose}] = useHandle(refetch);
+  const [
+    {columns, visible, editId, menuUserId, menuVisible},
+    {onAdd, onClose, onMenuClose},
+  ] = useHandle(refetch);
   const [isExporting, onExport] = useTableExportEvent({
     fn: exportUser,
     pageContext,
@@ -53,6 +57,13 @@ const TableList: FC = function () {
         onFetch={refetch}
         id={editId}
       />
+
+      <TreeModal
+        visible={menuVisible}
+        id={menuUserId}
+        onClose={onMenuClose}
+        onFetch={refetch}
+      />
     </>
   );
 };

+ 4 - 1
packages/app/src/pages/user/table/modal/hooks.ts

@@ -72,7 +72,10 @@ export function useFormState({
     userRole,
     userRealName,
   }) {
-    const params: Omit<EditUserParams, 'id' | 'modifyTime' | 'modifyUser'> = {
+    const params: Omit<
+      EditUserParams,
+      'id' | 'modifyTime' | 'modifyUser' | 'menu'
+    > = {
       userName,
       realName: userRealName,
       email: userEmail,

+ 11 - 0
packages/app/src/pages/user/table/tree-modal/context.ts

@@ -0,0 +1,11 @@
+import {useState} from 'react';
+import {createContext} from 'use-context-selector';
+
+export function useContextState() {
+  return useState<string[]>([]);
+}
+
+export const context = createContext<ReturnType<typeof useContextState>>([
+  [],
+  () => null,
+]);

+ 131 - 0
packages/app/src/pages/user/table/tree-modal/hooks.ts

@@ -0,0 +1,131 @@
+import {useContextSection} from '@hooks';
+import {context} from './context';
+import {FormEvent, useEffect} from 'react';
+import {useMutation, useQueryClient} from '@tanstack/react-query';
+import {editUserMenu, getRoleList, getTreeMenu} from '@apis';
+import {message} from 'antd';
+import {TreeMenuListData, UserListData} from '@models';
+
+function useEditMenu(onClose: () => void, onFetch: () => void) {
+  const {isLoading, mutate} = useMutation(editUserMenu, {
+    onSuccess({msg}) {
+      if (msg === '200') {
+        message.success('设置成功');
+        onClose();
+        onFetch();
+      }
+    },
+  });
+
+  return [isLoading, mutate] as const;
+}
+
+function useTreeMap() {
+  const client = useQueryClient();
+
+  function parseParentIdMap() {
+    const map = new Map<string, string>();
+
+    const treeData = client.getQueryData<TreeMenuListData[]>([
+      getTreeMenu.name,
+    ]);
+    if (!treeData || treeData.length === 0) return map;
+
+    function deepTree(tree: TreeMenuListData[]) {
+      tree.forEach(function ({pId, key, children}) {
+        // 一级菜单不参与寻找父级id
+        pId !== '0' && map.set(key, pId);
+
+        children.length && deepTree(children);
+      });
+    }
+
+    deepTree(treeData);
+
+    return map;
+  }
+
+  return parseParentIdMap;
+}
+
+export function useSubmit({
+  id,
+  onClose,
+  onFetch,
+}: {
+  id: string;
+  onClose: () => void;
+  onFetch: () => void;
+}) {
+  const list = useContextSection(context, ([list]) => list);
+  const [isLoading, mutate] = useEditMenu(onClose, onFetch);
+  const parseTreeMap = useTreeMap();
+
+  function parseMenuTreeList(list: string) {
+    const trees = list.split(',');
+    const menuSet = new Set(),
+      antdSet = new Set();
+    const treeMap = parseTreeMap();
+
+    trees.forEach(function (val) {
+      // 传给后台的要子菜单和父菜单的id
+      menuSet.add(val);
+
+      const pId = treeMap.get(val);
+
+      // 如果有pId说明是二级菜单 antd中需要添加
+      // 同时需要向menuSet中添加 确保所有的二级菜单的父级都包含
+
+      if (pId) {
+        menuSet.add(pId);
+        antdSet.add(val);
+      }
+    });
+
+    const menu = [...menuSet].join(','),
+      antd = [...antdSet].join(',');
+
+    return {
+      menu: `${menu}${menu ? ',' : ''}`,
+      antd,
+    };
+  }
+
+  function onSubmit(e: FormEvent<HTMLFormElement>) {
+    e.preventDefault();
+
+    const listStr = list.join(',');
+    const {menu} = parseMenuTreeList(listStr);
+
+    mutate({
+      id,
+      menu,
+    });
+  }
+
+  return [isLoading, onSubmit] as const;
+}
+
+export function useWatchId(id: string, visible: boolean) {
+  const client = useQueryClient();
+  const dispatch = useContextSection(context, ([, dispatch]) => dispatch);
+
+  useEffect(
+    function () {
+      if (!visible) return;
+
+      const data = client.getQueryData<UserListData[]>([getRoleList.name], {
+        exact: false,
+      });
+
+      const res = data?.find(val => String(val.id) === id);
+
+      if (res) {
+        const menuList = res?.menu ? res.menu.split(',').slice(0, -1) : [];
+
+        dispatch(menuList);
+      }
+    },
+    [id, client, dispatch, visible],
+  );
+}

+ 51 - 0
packages/app/src/pages/user/table/tree-modal/index.tsx

@@ -0,0 +1,51 @@
+import {ErrorBoundary, Loading, Modal} from '@components';
+import {FC, Suspense} from 'react';
+import TreeInfo from './info';
+import {ChildrenFC} from '@utils';
+import {context, useContextState} from './context';
+import {useSubmit, useWatchId} from './hooks';
+
+const TreeModalProvider: ChildrenFC = function ({children}) {
+  const state = useContextState();
+  const {Provider} = context;
+
+  return <Provider value={state}>{children}</Provider>;
+};
+type Props = {
+  id: string;
+  onClose: () => void;
+  onFetch: () => void;
+  visible: boolean;
+};
+
+const TreeModal: FC<Props> = function ({id, visible, onClose, onFetch}) {
+  const [isLoading, onSubmit] = useSubmit({id, onClose, onFetch});
+  useWatchId(id, visible);
+
+  return (
+    <Modal
+      title='菜单权限'
+      visible={visible}
+      isLoading={isLoading}
+      onSubmit={onSubmit}
+      onClose={onClose}
+      testId='role_tree_modal'
+    >
+      <ErrorBoundary>
+        <Suspense fallback={<Loading tip='正在获取菜单' height='200px' />}>
+          <TreeInfo />
+        </Suspense>
+      </ErrorBoundary>
+    </Modal>
+  );
+};
+
+const TreeModalWrapper: FC<Props> = function (props) {
+  return (
+    <TreeModalProvider>
+      <TreeModal {...props} />
+    </TreeModalProvider>
+  );
+};
+
+export default TreeModalWrapper;

+ 30 - 0
packages/app/src/pages/user/table/tree-modal/info/hooks.ts

@@ -0,0 +1,30 @@
+import {getTreeMenu} from '@apis';
+import {useContext} from '@hooks';
+import {useQuery} from '@tanstack/react-query';
+import {context} from '../context';
+
+export function useMenuTree() {
+  const {data} = useQuery({
+    queryKey: [getTreeMenu.name],
+    async queryFn() {
+      const data = await getTreeMenu();
+
+      if (data.msg === '200') return data.data;
+
+      throw new Error(data.errMsg);
+    },
+    suspense: true,
+  });
+
+  return data;
+}
+
+export function useCheck() {
+  const [list, setList] = useContext(context);
+
+  function onCheck(keys: string[]) {
+    setList(keys);
+  }
+
+  return [list, {onCheck}] as const;
+}

+ 4 - 0
packages/app/src/pages/user/table/tree-modal/info/index.module.css

@@ -0,0 +1,4 @@
+.tree-wrapper {
+  height: 300px;
+  overflow: auto;
+}

+ 23 - 0
packages/app/src/pages/user/table/tree-modal/info/index.tsx

@@ -0,0 +1,23 @@
+import {FC} from 'react';
+import {useCheck, useMenuTree} from './hooks';
+import {Tree} from 'antd';
+
+const TreeInfo: FC = function () {
+  const data = useMenuTree();
+  const [list, {onCheck}] = useCheck();
+
+  return (
+    <Tree
+      checkable
+      treeData={data}
+      onCheck={onCheck as any}
+      checkedKeys={list}
+      defaultExpandAll
+      height={300}
+      data-testid='role_tree'
+      style={{width: '240px', margin: '0 auto'}}
+    />
+  );
+};
+
+export default TreeInfo;