Browse Source

refactor: tabs 提取到stores

xyh 2 years ago
parent
commit
39f42fee07

+ 0 - 98
packages/app/src/pages/home/context.ts

@@ -1,98 +0,0 @@
-import {tabStore} from '@stores';
-import {useReducer} from 'react';
-import {createContext} from 'use-context-selector';
-
-type State = {
-  key: string;
-  url: string;
-  label: string;
-}[];
-
-type Action =
-  | {type: 'ADD'; payload: State[0]}
-  | {type: 'REMOVE'; payload: string}
-  | {type: 'REMOVE_OTHER'; payload: string}
-  | {type: 'REMOVE_RIGHT'; payload: string}
-  | {type: 'CLEAR'}
-  | {type: 'SORT'; payload: State};
-
-const defaultTab: State[0] = {key: '-1', url: '/main', label: '首页'};
-
-function reducer(state: State, action: Action): State {
-  const {type} = action;
-  const {key, dispatch: update} = tabStore.getState();
-
-  // 防止react devtool报错
-  function dispatch(key: string) {
-    Promise.resolve().then(() => update(key));
-  }
-
-  switch (type) {
-    case 'ADD': {
-      const {payload} = action;
-      setTimeout(function () {
-        dispatch(payload.key);
-      }, 0);
-
-      const exist = state.find(val => val.key === payload.key);
-      if (exist) {
-        return state;
-      }
-
-      return [...state, payload];
-    }
-    case 'REMOVE': {
-      const {payload} = action;
-      const idx = state.findIndex(val => val.key === payload);
-      if (idx < 0) return state;
-
-      key === state[idx].key && dispatch(state[idx - 1].key);
-
-      const nextState = [...state];
-      nextState.splice(idx, 1);
-      return nextState;
-    }
-    case 'REMOVE_OTHER': {
-      const {payload} = action;
-      const idx = state.findIndex(val => val.key === payload);
-      if (idx < 0) return state;
-
-      dispatch(payload);
-
-      return [{...defaultTab}, {...state[idx]}];
-    }
-    case 'REMOVE_RIGHT': {
-      const {payload} = action;
-      const idx = state.findIndex(val => val.key === payload);
-      if (idx < 0) return state;
-
-      const {length} = state;
-      const nextState = [...state];
-      nextState.splice(idx + 1, length - idx - 1);
-
-      if (nextState.findIndex(val => val.key === key) < 0) {
-        dispatch(payload);
-      }
-
-      return nextState;
-    }
-    case 'CLEAR': {
-      dispatch('-1');
-
-      return [{...defaultTab}];
-    }
-    case 'SORT':
-      return action.payload;
-    default:
-      return state;
-  }
-}
-
-export function useContextReudcer() {
-  return useReducer(reducer, [defaultTab]);
-}
-
-export const context = createContext<ReturnType<typeof useContextReudcer>>([
-  [],
-  () => null,
-]);

+ 4 - 15
packages/app/src/pages/home/index.tsx

@@ -9,22 +9,13 @@ import HomeSkeleton from './Skeleton';
 import BtnGroup from './btn-group';
 import FullScreenBtn from './fullscreen';
 import Main from './main';
-import {ChildrenFC} from '@utils';
-import {context, useContextReudcer} from './context';
 import {useStore} from 'zustand';
 import {tabStore} from '@stores';
 
-const TabProvider: ChildrenFC = function ({children}) {
-  const state = useContextReudcer();
-  const {Provider} = context;
-
-  return <Provider value={state}>{children}</Provider>;
-};
-
 const Home: FC = function () {
-  const dispatch = useStore(tabStore, state => state.dispatch);
+  const setActiveKey = useStore(tabStore, state => state.setActiveKey);
   function toHome() {
-    dispatch('-1');
+    setActiveKey('-1');
   }
 
   return (
@@ -41,10 +32,8 @@ const Home: FC = function () {
       </Layout.Header>
 
       <Layout hasSider>
-        <TabProvider>
-          <Menu />
-          <Main />
-        </TabProvider>
+        <Menu />
+        <Main />
       </Layout>
     </Layout>
   );

+ 38 - 24
packages/app/src/pages/home/main/hooks.tsx

@@ -1,13 +1,12 @@
-import {useContext, useContextSection} from '@hooks';
-import {context} from '../context';
 import {Modal, TabPaneProps, TabsProps} from 'antd';
-import {ReactNode} from 'react';
+import {ReactNode, useMemo} from 'react';
 import css from './index.module.css';
 import {useStore} from 'zustand';
 import {tabStore} from '@stores';
 import {useContextMenu, ItemParams} from 'react-contexify';
 import {DragEndEvent} from '@dnd-kit/core';
 import {arrayMove} from '@dnd-kit/sortable';
+import {shallowEqual} from '@utils';
 
 export type Tab = {
   key: string;
@@ -17,25 +16,34 @@ export type Tab = {
 export function useTabItems() {
   const {host} = location;
 
-  const tabs = useContextSection(context, function ([tabs]) {
-    return tabs.map<Tab>(function (tab) {
-      return {
-        ...tab,
-        originalTab: tab,
-        forceRender: true,
-        closable: tab.key !== '-1',
-        animated: true,
-        children: (
-          <iframe
-            src={`http://${host}${tab.url}`}
-            className={css.iframe}
-            data-url={tab.url}
-          />
-        ),
-      };
-    });
+  const {originalTab, dispatch} = useStore(tabStore, function (state) {
+    return {
+      dispatch: state.dispatch,
+      originalTab: state.tabList,
+    };
   });
-  const [originalTab, dispatch] = useContext(context);
+
+  const tabs = useMemo(
+    function () {
+      return originalTab.map<Tab>(function (tab) {
+        return {
+          ...tab,
+          originalTab: tab,
+          forceRender: true,
+          closable: tab.key !== '-1',
+          animated: true,
+          children: (
+            <iframe
+              src={`http://${host}${tab.url}`}
+              className={css.iframe}
+              data-url={tab.url}
+            />
+          ),
+        };
+      });
+    },
+    [host, originalTab],
+  );
 
   function onDragEnd({active, over}: DragEndEvent) {
     if (!over) return;
@@ -55,8 +63,14 @@ export function useTabItems() {
 }
 
 export function useTabActive() {
-  const dispatch = useContextSection(context, state => state[1]);
-  const {key: activeKey, dispatch: setActiveKey} = useStore(tabStore);
+  const dispatch = useStore(tabStore, state => state.dispatch);
+  const {activeKey, setActiveKey} = useStore(
+    tabStore,
+    function (state) {
+      return {activeKey: state.activeKey, setActiveKey: state.setActiveKey};
+    },
+    shallowEqual,
+  );
 
   function onClear() {
     Modal.confirm({
@@ -79,7 +93,7 @@ export function useTabActive() {
 
 export function useMenu() {
   const {show} = useContextMenu({id: 'content_menu'});
-  const dispatch = useContextSection(context, state => state[1]);
+  const dispatch = useStore(tabStore, state => state.dispatch);
 
   function onMenuitemClick(e: ItemParams<{key: string}, any>) {
     const {props, id} = e;

+ 2 - 4
packages/app/src/pages/home/main/tab-panel/index.tsx

@@ -1,16 +1,14 @@
-import {useContextSection} from '@hooks';
 import css from './index.module.css';
 import {FC, ReactNode, useEffect, useReducer, useRef} from 'react';
-import {context} from '../../context';
 import classNames from 'classnames';
 import {useStore} from 'zustand';
 import {tabStore} from '@stores';
 
 const TabPanel: FC = function () {
-  const tabs = useContextSection(context, state => state[0]);
+  const tabs = useStore(tabStore, state => state.tabList);
   const nodes = useRef<Map<string, ReactNode>>(new Map());
   const [, forceupdate] = useReducer(() => ({}), {});
-  const {key: activeKey} = useStore(tabStore);
+  const activeKey = useStore(tabStore, state => state.activeKey);
 
   useEffect(
     function () {

+ 4 - 6
packages/app/src/pages/home/main/tab/hooks.ts

@@ -1,13 +1,11 @@
-import {useContextSection} from '@hooks';
 import {CSSProperties, useEffect, useRef, useState} from 'react';
-import {context} from '../../context';
 import {useStore} from 'zustand';
 import {tabStore} from '@stores';
 
 export function useIndicator() {
   const [style, setStyle] = useState<CSSProperties>({display: 'none'});
-  const tabs = useContextSection(context, state => state[0]);
-  const {key} = useStore(tabStore);
+  const tabs = useStore(tabStore, state => state.tabList);
+  const key = useStore(tabStore, state => state.activeKey);
 
   useEffect(
     function () {
@@ -27,8 +25,8 @@ export function useIndicator() {
 
 export function useScrollToView() {
   const visibleTab = useRef<Set<string>>(new Set());
-  const tabs = useContextSection(context, state => state[0]);
-  const {key} = useStore(tabStore);
+  const tabs = useStore(tabStore, state => state.tabList);
+  const key = useStore(tabStore, state => state.activeKey);
 
   useEffect(
     function () {

+ 3 - 3
packages/app/src/pages/home/main/tab/index.tsx

@@ -1,10 +1,10 @@
 import css from './index.module.css';
 import {FC} from 'react';
-import {useContextSection} from '@hooks';
-import {context} from '../../context';
 import TabItem from './item';
 import {ShowContextMenuParams} from 'react-contexify';
 import {useScrollToView} from './hooks';
+import {useStore} from 'zustand';
+import {tabStore} from '@stores';
 
 type MakeOptional<Type, Key extends keyof Type> = Omit<Type, Key> &
   Partial<Pick<Type, Key>>;
@@ -16,7 +16,7 @@ type Props = {
 };
 
 const Tabs: FC<Props> = function ({onTabItemContentMenu}) {
-  const tabs = useContextSection(context, state => state[0]);
+  const tabs = useStore(tabStore, state => state.tabList);
   useScrollToView();
 
   return (

+ 9 - 4
packages/app/src/pages/home/main/tab/item/index.tsx

@@ -6,9 +6,8 @@ import classNames from 'classnames';
 import {useSortable} from '@dnd-kit/sortable';
 import {ShowContextMenuParams} from 'react-contexify';
 import {CloseSmall} from '@icon-park/react';
-import {useContextSection} from '@hooks';
-import {context} from '@pages/home/context';
 import {Modal} from 'antd';
+import {shallowEqual} from '@utils';
 
 type MakeOptional<Type, Key extends keyof Type> = Omit<Type, Key> &
   Partial<Pick<Type, Key>>;
@@ -22,10 +21,16 @@ type Props = {
 };
 
 const TabItem: FC<Props> = function ({id, label, onContentMenu}) {
-  const {key: activeKey, dispatch: setActiveKey} = useStore(tabStore);
+  const {activeKey, setActiveKey} = useStore(
+    tabStore,
+    function (state) {
+      return {activeKey: state.activeKey, setActiveKey: state.setActiveKey};
+    },
+    shallowEqual,
+  );
   const {setNodeRef, transform, transition, attributes, listeners} =
     useSortable({id, attributes: {role: 'tab'}});
-  const dispatch = useContextSection(context, state => state[1]);
+  const dispatch = useStore(tabStore, state => state.dispatch);
 
   function onClose(e: MouseEvent) {
     e.stopPropagation();

+ 4 - 6
packages/app/src/pages/home/menu/hooks.tsx

@@ -1,13 +1,11 @@
 import {getRoleMenu} from '@apis';
-import {useContextSection} from '@hooks';
-import {HOME_PATH} from '@routes';
+import {MAIN_PATH} from '@routes';
 import {menuStore, tabStore} from '@stores';
 import {useQuery} from '@tanstack/react-query';
 import {MENU_COLLAPSED_STORAGE, sortMenu} from '@utils';
 import {useBoolean, useLocalStorageState} from 'ahooks';
 import {useEffect, useState} from 'react';
 import {useStore} from 'zustand';
-import {context} from '../context';
 
 export function useMenu() {
   const setMenus = useStore(menuStore, state => state.setMenu);
@@ -22,7 +20,7 @@ export function useMenu() {
           {
             id: '-1',
             name: '首页',
-            url: HOME_PATH,
+            url: MAIN_PATH,
             pId: '0',
             page: 1,
             limit: 0,
@@ -54,8 +52,8 @@ export function useMenuState() {
   }
 
   const pages = useStore(menuStore, state => state.menus);
-  const activeKey = useStore(tabStore, state => state.key);
-  const dispatch = useContextSection(context, state => state[1]);
+  const activeKey = useStore(tabStore, state => state.activeKey);
+  const dispatch = useStore(tabStore, state => state.dispatch);
   function onClick(e: {key: string; keyPath: string[]}) {
     setOpenKey(e.keyPath);
     const data = pages.find(val => val.id === e.key);

+ 117 - 5
packages/app/src/stores/tab.ts

@@ -1,16 +1,128 @@
 import {createStore} from 'zustand/vanilla';
 
-type State = {key: string};
+type State = {
+  activeKey: string;
+  tabList: {
+    key: string;
+    url: string;
+    label: string;
+  }[];
+};
+type DispatchOptions =
+  | {type: 'ADD'; payload: State['tabList'][0]}
+  | {type: 'REMOVE'; payload: string}
+  | {type: 'REMOVE_OTHER'; payload: string}
+  | {type: 'REMOVE_RIGHT'; payload: string}
+  | {type: 'CLEAR'}
+  | {type: 'SORT'; payload: State['tabList']};
+
 type Action = {
-  dispatch: (key: string | ((prev: string) => string)) => void;
+  setActiveKey: (key: string | ((prev: string) => string)) => void;
+  dispatch: (value: DispatchOptions) => void;
 };
 
+const defaultTab: State['tabList'][0] = {
+  key: '-1',
+  url: '/main',
+  label: '首页',
+};
+
+function reducer(
+  state: State['tabList'],
+  action: DispatchOptions,
+): State['tabList'] {
+  const {type} = action;
+
+  switch (type) {
+    case 'ADD': {
+      const {payload} = action;
+
+      const exist = state.find(val => val.key === payload.key);
+      if (exist) {
+        return state;
+      }
+
+      return [...state, payload];
+    }
+    case 'REMOVE': {
+      const {payload} = action;
+      const idx = state.findIndex(val => val.key === payload);
+      if (idx < 0) return state;
+
+      const nextState = [...state];
+      nextState.splice(idx, 1);
+      return nextState;
+    }
+    case 'REMOVE_OTHER': {
+      const {payload} = action;
+      const idx = state.findIndex(val => val.key === payload);
+      if (idx < 0) return state;
+
+      return [{...defaultTab}, {...state[idx]}];
+    }
+    case 'REMOVE_RIGHT': {
+      const {payload} = action;
+      const idx = state.findIndex(val => val.key === payload);
+      if (idx < 0) return state;
+
+      const {length} = state;
+      const nextState = [...state];
+      nextState.splice(idx + 1, length - idx - 1);
+
+      return nextState;
+    }
+    case 'CLEAR': {
+      return [{...defaultTab}];
+    }
+    case 'SORT':
+      return action.payload;
+    default:
+      return state;
+  }
+}
+
 export const tabStore = createStore<State & Action>(function (set) {
   return {
-    key: '-1',
-    dispatch(key) {
+    activeKey: '-1',
+    tabList: [defaultTab],
+    setActiveKey(key) {
+      set(function (prev) {
+        return {activeKey: typeof key === 'string' ? key : key(prev.activeKey)};
+      });
+    },
+    dispatch(value) {
       set(function (prev) {
-        return {key: typeof key === 'string' ? key : key(prev.key)};
+        let nextActiveKey = prev.activeKey;
+        const result = reducer(prev.tabList, value);
+
+        switch (value.type) {
+          case 'ADD':
+            nextActiveKey = value.payload.key;
+            break;
+          case 'REMOVE': {
+            const idx = prev.tabList.findIndex(
+              val => val.key === value.payload,
+            );
+            idx >= 0 && (nextActiveKey = prev.tabList[idx].key);
+            break;
+          }
+          case 'REMOVE_OTHER':
+            nextActiveKey = value.payload;
+            break;
+          case 'REMOVE_RIGHT': {
+            result.findIndex(val => val.key === prev.activeKey) < 0 &&
+              (nextActiveKey = value.payload);
+            break;
+          }
+          case 'CLEAR':
+            nextActiveKey = '-1';
+            break;
+          case 'SORT': {
+            break;
+          }
+        }
+
+        return {tabList: result, activeKey: nextActiveKey};
       });
     },
   };