Skip to content

Commit

Permalink
feat(app.tsx): ✨ [App] add 标签页
Browse files Browse the repository at this point in the history
  • Loading branch information
jsxiaosi committed Dec 18, 2022
1 parent 6ecee07 commit 7cb8661
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 44 deletions.
2 changes: 1 addition & 1 deletion mock/demo/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const adminRoute = [
export default [
{
url: '/mock_api/getRoute',
timeout: 0,
timeout: 3000,
method: 'post',
response: ({ body }: { body: Recordable }) => {
const { name } = body;
Expand Down
7 changes: 4 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConfigProvider, Spin, theme } from 'antd';
import { ConfigProvider, theme } from 'antd';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import 'dayjs/locale/en';
Expand All @@ -15,6 +15,7 @@ import { useAppSelector } from './store/hooks';
import { getStorage } from './utils/storage';
import type { UseInfoType } from './server/useInfo';
import { initAsyncRoute } from './router/utils';
import LayoutSpin from './components/LayoutSpin';

function App() {
const { locale, color, themeMode } = useAppSelector(
Expand Down Expand Up @@ -64,10 +65,10 @@ function App() {
>
<IntlProvider locale={locale} messages={localeConfig[locale]}>
{loading ? (
<Spin size="large" />
<LayoutSpin />
) : (
// <BrowserRouter>
<Suspense fallback={<Spin size="large" />}>
<Suspense fallback={<LayoutSpin />}>
<Pages />
</Suspense>
// </BrowserRouter>
Expand Down
47 changes: 25 additions & 22 deletions src/Pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,37 @@ import { useState, useEffect, memo } from 'react';
import { baseRouter, errorPage } from './router';
import { useAppSelector } from './store/hooks';
import { handlePowerRoute, handleRouteList } from './router/utils';
import type { AsyncRouteType } from './store/modules/route';

// 为“/”根路由添加重定向
const handleRedirect = (asyncRouter: AsyncRouteType[]) => {
const routerList = handleRouteList(handlePowerRoute(asyncRouter));
if (routerList.length) {
routerList.push({
path: '',
element: <Navigate to={routerList[0].path || ''} />,
});
}
return [...routerList, ...errorPage];
};

const mapBaseRouter = (baseRouter: RouteObject[], asyncRouter: AsyncRouteType[]) => {
return baseRouter.map((i) => {
const routeItem = i;
if (routeItem.path === '/') {
routeItem.children = handleRedirect(asyncRouter);
}
return routeItem;
});
};

const Pages = memo(() => {
const [route, setRoute] = useState<RouteObject[]>(baseRouter);
const asyncRouter = useAppSelector((state) => state.route.asyncRouter);

// 为“/”根路由添加重定向
const handleRedirect = () => {
const routerList = handleRouteList(handlePowerRoute(asyncRouter));
if (routerList.length) {
routerList.push({
path: '',
element: <Navigate to={routerList[0].path || ''} />,
});
}
return [...routerList, ...errorPage];
};
const [route, setRoute] = useState<RouteObject[]>(mapBaseRouter(baseRouter, asyncRouter));

// 更新路由列表
useEffect(() => {
setRoute(
baseRouter.map((i) => {
const routeItem = i;
if (routeItem.path === '/') {
routeItem.children = handleRedirect();
}
return routeItem;
}),
);
setRoute(mapBaseRouter(baseRouter, asyncRouter));
}, [asyncRouter]);

const routeElemt = createBrowserRouter(route);
Expand Down
12 changes: 12 additions & 0 deletions src/components/LayoutSpin/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Spin } from 'antd';
import { memo } from 'react';

const LayoutSpin = memo(() => {
return (
<div className="supense-loading">
<Spin size="large" />
</div>
);
});

export default LayoutSpin;
12 changes: 10 additions & 2 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ body {
padding: 0;
}

:root {
#root {
width: 100%;
height: 100%;
height: 100vh;
transition: background 0.3s, width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s;
}

Expand All @@ -18,6 +18,14 @@ ul {
margin: 0;
}

.supense-loading {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

.cursor {
cursor: pointer;
}
15 changes: 12 additions & 3 deletions src/layout/components/AppMain/AppMain.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { Layout } from 'antd';
import { memo, Suspense } from 'react';
import { Outlet } from 'react-router-dom';
import { KeepAlive } from './KeepAlive';
import { getAppMainStyle } from './style';
import TabsPage from './TabsPage';
import LayoutSpin from '@/components/LayoutSpin';

const { Content } = Layout;

const AppMain = memo(() => {
const isKeepAlive = true;
return (
<Content css={getAppMainStyle()}>
<TabsPage />
<div className="main-content">
<Suspense>
<Outlet />
</Suspense>
{isKeepAlive ? (
<KeepAlive />
) : (
<Suspense fallback={<LayoutSpin />}>
<Outlet />
</Suspense>
)}
</div>
</Content>
);
Expand Down
113 changes: 113 additions & 0 deletions src/layout/components/AppMain/KeepAlive/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import type { RefObject, ReactNode } from 'react';
import React, { Suspense, memo, useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useLocation, useOutlet } from 'react-router-dom';
import { useAppSelector } from '@/store/hooks';
import LayoutSpin from '@/components/LayoutSpin';

interface Props extends ComponentReactElement {
include?: Array<string>;
maxLen?: number;
}
export const KeepAlive = memo(({ maxLen = 10 }: Props) => {
const ele = useOutlet();
const location = useLocation();
const activeName = location.pathname;
const multiTabs = useAppSelector((state) => state.route.multiTabs);
const levelAsyncRouter = useAppSelector((state) => state.route.levelAsyncRouter);

const containerRef = useRef<HTMLDivElement>(null);
const [cacheReactNodes, setCacheReactNodes] = useState<Array<{ name: string; ele?: ReactNode }>>(
[],
);

useEffect(() => {
if (!activeName) {
return;
}
const include = multiTabs.map((i) => i.key);
const levelRouter = levelAsyncRouter.map((i) => i.path);
setCacheReactNodes((reactNodes) => {
// 缓存超过上限的
if (reactNodes.length >= maxLen) {
reactNodes = reactNodes.slice(0, 1);
}
// 添加
const reactNode = reactNodes.find((res) => res.name === activeName);
if (!reactNode) {
reactNodes.push({
name: activeName,
ele: ele,
});
} else {
// 权限判断
const nodeParom = reactNodes
.filter((i) => !levelRouter.includes(i.name))
.map((i) => i.name);
const reactIndex = reactNodes.findIndex((res) => nodeParom.includes(res.name));
if (reactIndex !== -1) reactNodes[reactIndex].ele = ele;
}

// 缓存路由列表和标签页列表同步
if (include) {
return reactNodes.filter((i) => include.includes(i.name));
}
return reactNodes;
});
}, [activeName, maxLen, multiTabs, levelAsyncRouter]);

return (
<>
<div ref={containerRef} className="keep-alive" />
{cacheReactNodes.map((i) => {
return (
<Component
active={i.name === activeName}
renderDiv={containerRef}
name={i.name}
key={i.name}
>
{i.ele}
</Component>
);
})}
</>
);
});

export interface ComponentReactElement {
children?: ReactNode | ReactNode[];
}

interface ComponentProps extends ComponentReactElement {
active: boolean;
name: string;
renderDiv: RefObject<HTMLDivElement>;
}

export const Component: React.FC<ComponentProps> = ({ active, children, name, renderDiv }) => {
const [targetElement] = useState(() => document.createElement('div'));
const activatedRef = useRef(false);
activatedRef.current = activatedRef.current || active;

useEffect(() => {
if (active) {
renderDiv.current?.appendChild(targetElement);
} else {
try {
renderDiv.current?.removeChild(targetElement);
// eslint-disable-next-line no-empty
} catch (e) {}
}
}, [active, name, renderDiv, targetElement]);

useEffect(() => {
targetElement.setAttribute('id', name);
}, [name, targetElement]);

return (
<Suspense fallback={<LayoutSpin />}>
{activatedRef.current && createPortal(children, targetElement)}
</Suspense>
);
};
82 changes: 82 additions & 0 deletions src/layout/components/AppMain/TabsPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Tabs } from 'antd';
import { memo, useEffect, useMemo } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { findRouteByPath, routeListToMenu } from '@/router/utils';
import { setStoreMultiTabs } from '@/store/modules/route';
import defaultRoute from '@/router/modules';

const TabsPage = memo(() => {
const location = useLocation();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const menuList = routeListToMenu(defaultRoute);
const asyncRouter = useAppSelector((state) => state.route.asyncRouter);
const multiTabs = useAppSelector((state) => state.route.multiTabs);

const tabsItem = useMemo(() => {
return multiTabs.map((i) => {
const routeBy = findRouteByPath(i.key, menuList);
return {
key: i.key,
label: routeBy?.label,
};
});
}, [multiTabs]);

const handleTabsList = (pathName: string, type: 'add' | 'delete') => {
const oldKeyAliveList = [...multiTabs];

const tabIndex = oldKeyAliveList.findIndex((i) => i.key === pathName);
switch (type) {
case 'add':
if (tabIndex === -1) {
oldKeyAliveList.push({ key: pathName });
}
break;
case 'delete':
if (tabIndex !== -1) oldKeyAliveList.splice(tabIndex, 1);
break;
default:
break;
}

dispatch(setStoreMultiTabs(oldKeyAliveList));
};

const onEdit = (
targetKey: React.MouseEvent | React.KeyboardEvent | string,
action: 'add' | 'remove',
) => {
if (action === 'remove') {
const muIndex = multiTabs.findIndex((i) => i.key === targetKey);
if (muIndex === multiTabs.length - 1) navigate(multiTabs[muIndex - 1].key);
else navigate(multiTabs[multiTabs.length - 1].key);
handleTabsList(targetKey as string, 'delete');
}
};

useEffect(() => {
if (location.pathname === '/') {
if (asyncRouter.length) navigate(asyncRouter[0].path);
return;
}
handleTabsList(location.pathname, 'add');
}, [location.pathname]);

return (
<Tabs
hideAdd
activeKey={location.pathname}
type={tabsItem.length > 1 ? 'editable-card' : 'card'}
onChange={(key) => navigate(key)}
onEdit={onEdit}
tabBarStyle={{
margin: 0,
}}
items={tabsItem}
/>
);
});

export default TabsPage;
3 changes: 3 additions & 0 deletions src/layout/components/AppMain/style.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import type { CSSObject } from '@emotion/react';

export const getAppMainStyle = (): CSSObject => {
return {
display: 'flex',
flexDirection: 'column',
['.main-content']: {
padding: 12,
height: '100%',
overflowY: 'auto',
position: 'relative',
},
};
};
Loading

0 comments on commit 7cb8661

Please sign in to comment.