Browse Source

refactor: 移除antd的tab自写tab 为了确保iframe不会rebuild tab拖拽可autoscroll

xyh 2 years ago
parent
commit
e39e9872ba

+ 5 - 14
cypress/e2e/tab.cy.ts

@@ -6,28 +6,19 @@ describe('page tab', function () {
   });
 
   function validateTabLength(length: number) {
-    cy.getTestId('tab_list')
-      .find('.ant-tabs-nav')
-      .find('.ant-tabs-nav-wrap')
-      .find('.ant-tabs-tab')
-      .should('have.length', length);
+    cy.get('#p_tab_list').find('li[role="tab"]').should('have.length', length);
   }
 
   function validateTabActiveText(text: string) {
-    cy.getTestId('tab_list')
-      .find('.ant-tabs-tab-active')
-      .children('div')
+    cy.get('#p_tab_list')
+      .find('li[data-isactive="1"]')
       .should('have.text', text);
   }
 
   function clickTab(eq: number, contextMenu = false) {
-    const el = cy
-      .getTestId('tab_list')
-      .find('.ant-tabs-nav-list')
-      .children('span')
-      .eq(eq);
+    const el = cy.get('#p_tab_list').find('li[role="tab"]').eq(eq);
 
-    contextMenu ? el.trigger('contextmenu') : el.find('.ant-tabs-tab').click();
+    contextMenu ? el.trigger('contextmenu') : el.click();
   }
 
   it('tab', function () {

+ 28 - 50
packages/app/src/pages/home/main/index.tsx

@@ -1,7 +1,6 @@
-import {Layout, Tabs} from 'antd';
+import {Layout} from 'antd';
 import {FC} from 'react';
-import css from './index.module.css';
-import {useMenu, useTabActive, useTabItems} from './hooks';
+import {useMenu, useTabItems} from './hooks';
 import {Menu, Item} from 'react-contexify';
 import {
   DndContext,
@@ -9,16 +8,19 @@ import {
   PointerSensor,
   closestCenter,
 } from '@dnd-kit/core';
-import {restrictToHorizontalAxis} from '@dnd-kit/modifiers';
+import {
+  restrictToFirstScrollableAncestor,
+  restrictToHorizontalAxis,
+} from '@dnd-kit/modifiers';
 import {
   SortableContext,
   horizontalListSortingStrategy,
 } from '@dnd-kit/sortable';
-import DraggableItem from './draggable-item';
+import Tabs from './tab';
+import TabPanel from './tab-panel';
 
 const Main: FC = function () {
   const [tabs, onDragEnd] = useTabItems();
-  const [activeKey, {onChange, onEdit}] = useTabActive();
   const {show, onMenuitemClick} = useMenu();
   const sensor = useSensor(PointerSensor, {
     activationConstraint: {distance: 10},
@@ -26,50 +28,26 @@ const Main: FC = function () {
 
   return (
     <>
-      <Layout.Content className='layout-content-custom'>
-        <Tabs
-          defaultActiveKey='-1'
-          data-testid='tab_list'
-          size='small'
-          hideAdd
-          activeKey={activeKey}
-          onChange={onChange}
-          items={tabs}
-          className={css.tabs}
-          onEdit={onEdit}
-          renderTabBar={function (props, DefaultTab) {
-            return (
-              <DndContext
-                modifiers={[restrictToHorizontalAxis]}
-                collisionDetection={closestCenter}
-                onDragEnd={onDragEnd}
-                sensors={[sensor]}
-                autoScroll
-              >
-                <SortableContext
-                  items={tabs.map(val => val.key)}
-                  strategy={horizontalListSortingStrategy}
-                >
-                  <DefaultTab {...props}>
-                    {function (node) {
-                      return (
-                        <DraggableItem
-                          {...node.props}
-                          id={node.key!.toString()}
-                          showContext={show}
-                          key={node.key}
-                        >
-                          {node}
-                        </DraggableItem>
-                      );
-                    }}
-                  </DefaultTab>
-                </SortableContext>
-              </DndContext>
-            );
-          }}
-        />
-      </Layout.Content>
+      <DndContext
+        modifiers={[
+          restrictToFirstScrollableAncestor,
+          restrictToHorizontalAxis,
+        ]}
+        collisionDetection={closestCenter}
+        onDragEnd={onDragEnd}
+        sensors={[sensor]}
+        autoScroll
+      >
+        <SortableContext
+          items={tabs.map(val => val.key)}
+          strategy={horizontalListSortingStrategy}
+        >
+          <Layout.Content className='layout-content-custom'>
+            <Tabs onTabItemContentMenu={show} />
+            <TabPanel />
+          </Layout.Content>
+        </SortableContext>
+      </DndContext>
 
       <Menu id='content_menu' style={{zIndex: '999999'}}>
         <Item id='remove' data-testid='remove' onClick={onMenuitemClick}>

+ 26 - 0
packages/app/src/pages/home/main/tab-panel/index.module.css

@@ -0,0 +1,26 @@
+.iframe {
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+  border: unset;
+}
+
+.tab-panel {
+  position: relative;
+  flex: 1;
+  width: 100%;
+  overflow: hidden;
+}
+
+.iframe-wrapper {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 1;
+  width: 100%;
+  height: 100%;
+}
+
+.iframe-top {
+  z-index: 999;
+}

+ 56 - 0
packages/app/src/pages/home/main/tab-panel/index.tsx

@@ -0,0 +1,56 @@
+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 nodes = useRef<Map<string, ReactNode>>(new Map());
+  const [, forceupdate] = useReducer(() => ({}), {});
+  const {key: activeKey} = useStore(tabStore);
+
+  useEffect(
+    function () {
+      // 如果长度相同 说明是修改顺序或者调整内容
+      if (tabs.length === nodes.current.size) return;
+      const map = nodes.current;
+      const nextMap = new Map<string, ReactNode>();
+
+      // 创建新的map 遍历查询内容 如果有复用 没有创建一个
+      tabs.forEach(function ({key, url}) {
+        if (map.has(key)) {
+          nextMap.set(key, map.get(key));
+        } else {
+          nextMap.set(key, <iframe src={url} className={css.iframe} />);
+        }
+      });
+
+      nodes.current = nextMap;
+
+      forceupdate();
+    },
+    [nodes, tabs],
+  );
+
+  return (
+    <div className={css.tabPanel}>
+      {Array.from(nodes.current.entries()).map(function ([key, node]) {
+        return (
+          <div
+            key={key}
+            className={classNames(css.iframeWrapper, {
+              [css.iframeTop]: key === activeKey,
+            })}
+          >
+            {node}
+          </div>
+        );
+      })}
+    </div>
+  );
+};
+
+export default TabPanel;

+ 70 - 0
packages/app/src/pages/home/main/tab/hooks.ts

@@ -0,0 +1,70 @@
+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 visibleTab = useRef<Set<string>>(new Set());
+  const tabs = useContextSection(context, state => state[0]);
+  const {key} = useStore(tabStore);
+
+  useEffect(
+    function () {
+      const observer = new IntersectionObserver(
+        function (entries) {
+          entries.forEach(function ({intersectionRatio, target}) {
+            const key = target.getAttribute('data-key')!;
+
+            if (intersectionRatio >= 1) {
+              visibleTab.current.add(key);
+            } else {
+              visibleTab.current.delete(key);
+            }
+          });
+        },
+        {
+          root: document.querySelector('#p_tab_list'),
+        },
+      );
+
+      document
+        .querySelector('#p_tab_list')
+        ?.querySelectorAll('li[role="tab"]')
+        ?.forEach(function (el) {
+          observer.observe(el);
+        });
+
+      return () => observer.disconnect();
+    },
+    [tabs],
+  );
+
+  useEffect(
+    function () {
+      const activeEl = document.getElementById(`p_tab_item_${key}`);
+      if (!activeEl) return setStyle({display: 'none'});
+
+      const {width} = activeEl.getBoundingClientRect();
+      const left = activeEl.offsetLeft;
+
+      setStyle({width, transform: `translate(${left}px)`});
+
+      // 判断是否需要滚动到指定位置
+      if (!visibleTab.current.has(key)) {
+        document
+          .querySelector('#p_tab_list')
+          ?.querySelector(`#p_tab_item_${key}`)
+          ?.scrollIntoView({
+            behavior: 'smooth',
+            inline: 'start',
+            block: 'start',
+          });
+      }
+    },
+    [key, tabs],
+  );
+
+  return style;
+}

+ 48 - 0
packages/app/src/pages/home/main/tab/index.module.css

@@ -0,0 +1,48 @@
+.tab-list {
+  --animate-duration: 150ms;
+  --animate-mode: linear;
+
+  position: relative;
+  display: flex;
+  width: 100%;
+  padding-bottom: 2px;
+  overflow: auto;
+  /* stylelint-disable-next-line declaration-block-no-duplicate-properties */
+  overflow: overlay;
+  border-bottom: 1px solid #eee;
+
+  &::-webkit-scrollbar {
+    display: none;
+    height: 4px;
+    background-color: transparent;
+    border-radius: 6px;
+    transition: all 500ms linear;
+  }
+
+  &:hover::-webkit-scrollbar {
+    display: block;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    cursor: pointer;
+    background-color: #eee;
+    border-radius: 6px;
+    transition: background-color 500ms linear;
+
+    &:hover {
+      background-color: var(--primary-color);
+    }
+  }
+}
+
+.tab-indicator {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  width: 10px;
+  height: 2px;
+  background-color: var(--primary-color);
+  border-radius: 2px;
+  transition: width var(--animate-duration) var(--animate-mode),
+    transform var(--animate-duration) var(--animate-mode);
+}

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

@@ -0,0 +1,40 @@
+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 {useIndicator} from './hooks';
+
+type MakeOptional<Type, Key extends keyof Type> = Omit<Type, Key> &
+  Partial<Pick<Type, Key>>;
+
+type Props = {
+  onTabItemContentMenu: (
+    params: MakeOptional<ShowContextMenuParams<unknown>, 'id'>,
+  ) => void;
+};
+
+const Tabs: FC<Props> = function ({onTabItemContentMenu}) {
+  const tabs = useContextSection(context, state => state[0]);
+  const style = useIndicator();
+
+  return (
+    <ul className={css.tabList} id='p_tab_list'>
+      {tabs.map(function ({key, label}) {
+        return (
+          <TabItem
+            key={key}
+            label={label}
+            id={key}
+            onContentMenu={onTabItemContentMenu}
+          />
+        );
+      })}
+
+      <i className={css.tabIndicator} style={style} />
+    </ul>
+  );
+};
+
+export default Tabs;

+ 10 - 0
packages/app/src/pages/home/main/tab/item/index.module.css

@@ -0,0 +1,10 @@
+.tab-item {
+  flex-shrink: 0;
+  padding: 10px 16px;
+  font-size: 14px;
+  cursor: pointer;
+}
+
+.tab-item-active {
+  color: var(--primary-color);
+}

+ 54 - 0
packages/app/src/pages/home/main/tab/item/index.tsx

@@ -0,0 +1,54 @@
+import {tabStore} from '@stores';
+import css from './index.module.css';
+import {FC} from 'react';
+import {useStore} from 'zustand';
+import classNames from 'classnames';
+import {useSortable} from '@dnd-kit/sortable';
+import {ShowContextMenuParams} from 'react-contexify';
+
+type MakeOptional<Type, Key extends keyof Type> = Omit<Type, Key> &
+  Partial<Pick<Type, Key>>;
+
+type Props = {
+  id: string;
+  label: string;
+  onContentMenu: (
+    params: MakeOptional<ShowContextMenuParams<unknown>, 'id'>,
+  ) => void;
+};
+
+const TabItem: FC<Props> = function ({id, label, onContentMenu}) {
+  const {key: activeKey, dispatch} = useStore(tabStore);
+  const {setNodeRef, transform, transition, attributes, listeners} =
+    useSortable({id, attributes: {role: 'tab'}});
+
+  return (
+    <li
+      onContextMenu={e =>
+        id === '-1'
+          ? e.preventDefault()
+          : onContentMenu({event: e, props: {key: id}})
+      }
+      id={`p_tab_item_${id}`}
+      data-key={id}
+      data-isactive={id === activeKey ? '1' : '0'}
+      className={classNames(css.tabItem, {
+        [css.tabItemActive]: id === activeKey,
+      })}
+      onClick={() => dispatch(id)}
+      ref={setNodeRef}
+      {...attributes}
+      {...listeners}
+      style={{
+        transition,
+        transform: `translate3d(${transform?.x ?? 0}px, ${
+          transform?.y ?? 0
+        }px, 0)`,
+      }}
+    >
+      {label}
+    </li>
+  );
+};
+
+export default TabItem;