From ba7f7b75ff38ff6f1df3dde4fea4241bbddc5585 Mon Sep 17 00:00:00 2001 From: zzZ <> Date: Mon, 28 Oct 2024 15:54:33 +0800 Subject: [PATCH] feat: router/layout example --- README.md | 8 +- docs/.vitepress/config.ts | 3 +- docs/index.md | 3 + package.json | 3 +- public/images/react.svg | 1 + src/App.tsx | 4 +- src/assets/svg/back.svg | 1 + src/assets/svg/external_link.svg | 1 + src/assets/svg/menu2.svg | 1 + src/assets/svg/nintendo.svg | 1 + src/assets/svg/rectangle.svg | 1 + src/assets/svg/route.svg | 1 + src/assets/svg/single_slider.svg | 1 + src/assets/svg/web.svg | 1 + src/components/Back/index.less | 18 ++++ src/components/Back/index.tsx | 41 +++++++++ src/layouts/Footer/index.less | 1 + src/layouts/SideMenu/index.tsx | 23 +++-- src/layouts/SideMenu/utils.ts | 4 +- src/locales/en/components.json | 3 + src/locales/en/index.ts | 2 + src/locales/en/menu.json | 8 ++ src/locales/en/router.json | 22 +++++ src/locales/zh-Hans/components.json | 3 + src/locales/zh-Hans/index.ts | 2 + src/locales/zh-Hans/menu.json | 8 ++ src/locales/zh-Hans/router.json | 22 +++++ src/models/base/index.ts | 6 ++ src/pages/router/dynamic/index.tsx | 122 +++++++++++++++++++++++++++ src/pages/router/meta/index.tsx | 68 +++++++++++++++ src/pages/router/tempRoute/index.tsx | 15 ++++ src/pages/separation/index.tsx | 17 ++++ src/pages/singleSider/index.tsx | 11 +++ src/router/config/index.tsx | 41 +++++++++ src/utils/index.ts | 15 ++++ 35 files changed, 465 insertions(+), 17 deletions(-) create mode 100644 public/images/react.svg create mode 100644 src/assets/svg/back.svg create mode 100644 src/assets/svg/external_link.svg create mode 100644 src/assets/svg/menu2.svg create mode 100644 src/assets/svg/nintendo.svg create mode 100644 src/assets/svg/rectangle.svg create mode 100644 src/assets/svg/route.svg create mode 100644 src/assets/svg/single_slider.svg create mode 100644 src/assets/svg/web.svg create mode 100644 src/components/Back/index.less create mode 100644 src/components/Back/index.tsx create mode 100644 src/locales/en/components.json create mode 100644 src/locales/en/router.json create mode 100644 src/locales/zh-Hans/components.json create mode 100644 src/locales/zh-Hans/router.json create mode 100644 src/pages/router/dynamic/index.tsx create mode 100644 src/pages/router/meta/index.tsx create mode 100644 src/pages/router/tempRoute/index.tsx create mode 100644 src/pages/separation/index.tsx create mode 100644 src/pages/singleSider/index.tsx diff --git a/README.md b/README.md index 8691ea7..bd9d9f3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # 后台管理系统的前端解决方案 -[在线预览](https://template.react-antd-console.site) | [个人pro版在线预览](https://react-antd-console.site) | [文档](https://doc.react-antd-console.site) +[在线预览](https://template.react-antd-console.site) | [拓展pro版在线预览](https://react-antd-console.site) | [文档](https://doc.react-antd-console.site)

@@ -64,9 +64,9 @@ npm run build:prod ## Pro edition -本项目作为基础模板,是通用的。下面是根据本项目模板拓展出来的个人pro版本(未完待续),如果你认为哪些功能是通用的,有必要放进模板中,可以提出意见和建议,我们根据实际情况考虑放进去 +本项目作为基础模板,是通用的。下面是根据本项目模板拓展出来的拓展pro版本(未完待续),如果你认为哪些功能是通用的,有必要放进模板中,可以提出意见和建议,我们根据实际情况考虑放进去 -[个人pro版本预览](https://react-antd-console.site) +[拓展pro版在线预览](https://react-antd-console.site) ### 深/浅色主题 @@ -102,4 +102,4 @@ npm run build:prod -[在线预览](https://template.react-antd-console.site) | [个人pro版在线预览](https://react-antd-console.site) | [文档](https://doc.react-antd-console.site) +[在线预览](https://template.react-antd-console.site) | [拓展pro版在线预览](https://react-antd-console.site) | [文档](https://doc.react-antd-console.site) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 862e715..e42c40c 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -15,7 +15,8 @@ export default withMermaid({ nav: [ { text: '主页', link: '/' }, { text: '文档', link: '/guide/what' }, - { text: '在线预览', link: 'https://react-antd-console.site' } + { text: '在线预览', link: 'https://template.react-antd-console.site' }, + { text: '拓展pro版在线预览', link: 'https://react-antd-console.site' } ], sidebar: [ diff --git a/docs/index.md b/docs/index.md index 493f125..3e13b78 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,6 +13,9 @@ hero: link: /guide/what - theme: alt text: 在线预览 + link: https://template.react-antd-console.site + - theme: alt + text: 拓展pro版在线预览 link: https://react-antd-console.site features: diff --git a/package.json b/package.json index a40bc93..5a61c71 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "types": "./dist-lib/react-antd-console.d.ts", "scripts": { "start": "vite --mode localhost", + "start:force": "vite --force --mode localhost", "build:dev": "vite build --mode dev", "build:test": "vite build --mode test", "build:uat": "vite build --mode uat", @@ -46,7 +47,7 @@ "react-helmet-async": "^2.0.5", "react-i18next": "^15.0.2", "react-router-dom": "^6.26.2", - "react-router-toolset": "^0.0.2", + "react-router-toolset": "^0.0.5", "react-use": "^17.5.1", "store2": "^2.14.3" }, diff --git a/public/images/react.svg b/public/images/react.svg new file mode 100644 index 0000000..546942a --- /dev/null +++ b/public/images/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 214eb3c..d163d33 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,8 +6,8 @@ import { useTranslation } from 'react-i18next'; import '@/styles/index.less'; function App() { - const { curRoute } = useRouter(router); - const element = useRoutes(router.reactRoutes); + const { curRoute, reactRoutes } = useRouter(router); + const element = useRoutes(reactRoutes); const { t: t_menu } = useTranslation('menu'); return ( diff --git a/src/assets/svg/back.svg b/src/assets/svg/back.svg new file mode 100644 index 0000000..fbd89ff --- /dev/null +++ b/src/assets/svg/back.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svg/external_link.svg b/src/assets/svg/external_link.svg new file mode 100644 index 0000000..8563938 --- /dev/null +++ b/src/assets/svg/external_link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svg/menu2.svg b/src/assets/svg/menu2.svg new file mode 100644 index 0000000..fb39977 --- /dev/null +++ b/src/assets/svg/menu2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svg/nintendo.svg b/src/assets/svg/nintendo.svg new file mode 100644 index 0000000..bec7a81 --- /dev/null +++ b/src/assets/svg/nintendo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svg/rectangle.svg b/src/assets/svg/rectangle.svg new file mode 100644 index 0000000..a0dfec1 --- /dev/null +++ b/src/assets/svg/rectangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svg/route.svg b/src/assets/svg/route.svg new file mode 100644 index 0000000..19eb329 --- /dev/null +++ b/src/assets/svg/route.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svg/single_slider.svg b/src/assets/svg/single_slider.svg new file mode 100644 index 0000000..b6ae74b --- /dev/null +++ b/src/assets/svg/single_slider.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svg/web.svg b/src/assets/svg/web.svg new file mode 100644 index 0000000..8ff4366 --- /dev/null +++ b/src/assets/svg/web.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Back/index.less b/src/components/Back/index.less new file mode 100644 index 0000000..d606123 --- /dev/null +++ b/src/components/Back/index.less @@ -0,0 +1,18 @@ +.console__back { + display: inline-flex; + align-items: center; + margin-bottom: 16px; + .ant-divider { + margin-inline: 16px; + } +} + +.console__back-icon { + margin-right: 4px; +} + +.console__back-action { + display: flex; + align-items: center; + cursor: pointer; +} diff --git a/src/components/Back/index.tsx b/src/components/Back/index.tsx new file mode 100644 index 0000000..b253c08 --- /dev/null +++ b/src/components/Back/index.tsx @@ -0,0 +1,41 @@ +import { Divider } from 'antd'; +import { history } from '@/router'; +import { useTranslation } from 'react-i18next'; +import { FC } from 'react'; +import SvgIcon from '@/components/SvgIcon'; +import './index.less'; + +interface Props { + title?: string; + backUrl?: string; +} + +/** + * 页面返回 + */ +const Back: FC = ({ title, backUrl }) => { + const { t: t_components } = useTranslation('components'); + + function onClickBack() { + if (backUrl) { + history.push(backUrl); + } else { + history.back(); + } + } + + return ( +

+ + + {t_components('返回')} + + {title && <> + +

{title}

+ } +
+ ); +}; + +export default Back; diff --git a/src/layouts/Footer/index.less b/src/layouts/Footer/index.less index 3e03500..75af766 100644 --- a/src/layouts/Footer/index.less +++ b/src/layouts/Footer/index.less @@ -5,6 +5,7 @@ align-items: center; height: 60px; background-color: #fff; + border-top: 1px solid #0505050f; .anticon, svg { margin: 0 4px; } diff --git a/src/layouts/SideMenu/index.tsx b/src/layouts/SideMenu/index.tsx index 45ca13e..ff16a01 100644 --- a/src/layouts/SideMenu/index.tsx +++ b/src/layouts/SideMenu/index.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { Menu } from 'antd'; -import router from '@/router'; +import router, { useRouter } from '@/router'; import { history } from '@/router'; import type { MenuInfo } from 'rc-menu/lib/interface.d'; import { useMenuStatus } from './hooks'; @@ -9,7 +9,6 @@ import { baseModel } from '@/models/base'; import { withAuthModel } from '@/models/withAuth'; import { useModel } from '@zhangsai/model'; import { generateMenuItems } from './utils'; -import { logo } from '@/consts'; import SvgIcon from '@/components/SvgIcon'; import './index.less'; @@ -17,13 +16,15 @@ import './index.less'; * Layout菜单 */ const SideMenu = () => { + const logo = useModel(baseModel, 'logo'); const permissions = useModel(withAuthModel, 'permissions'); const language = useModel(baseModel, 'language'); + const { routes, flattenRoutes } = useRouter(router); /** 根据权限和语言生成菜单数据 */ const menuItems = useMemo(() => { - return generateMenuItems(router.routes, permissions); + return generateMenuItems(routes, permissions); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [permissions, language, router.routes]); + }, [permissions, language, routes]); /** 展开菜单 */ const [collapsed, setCollapsed] = useState(false); @@ -34,10 +35,16 @@ const SideMenu = () => { } = useMenuStatus(); function onClickMenuItem(info: MenuInfo) { - const { key, keyPath } = info; - setOpenKeys(keyPath.slice(1)); - setSelectedKeys([key]); - history.push(router.getPathname(key)); + const { key } = info; + const clickingRoute = flattenRoutes.get(key); + if (clickingRoute?.external) { + window.open(clickingRoute.path); + } else { + const { key, keyPath } = info; + setOpenKeys(keyPath.slice(1)); + setSelectedKeys([key]); + history.push(router.getPathname(key)); + } } function onOpenChange(_openKeys: string[]) { diff --git a/src/layouts/SideMenu/utils.ts b/src/layouts/SideMenu/utils.ts index fbc656e..12f6625 100644 --- a/src/layouts/SideMenu/utils.ts +++ b/src/layouts/SideMenu/utils.ts @@ -7,6 +7,7 @@ export interface ItemType extends MenuItemType { title?: string; children?: ItemType[]; parent?: ItemType; + external?: boolean; popupClassName?: string; } @@ -16,7 +17,7 @@ export interface ItemType extends MenuItemType { export function generateMenuItems(_routes: RouteConfig[], _permissions: Record, parent?: ItemType): ItemType[] { const ret: ItemType[] = []; for (let i = 0; i < _routes.length; i++) { - const { collecttedPathname = [], icon, name, hidden, flatten, children, redirect, permission } = _routes[i]; + const { collecttedPathname = [], icon, name, hidden, flatten, children, redirect, permission, external } = _routes[i]; if (redirect || (permission && !_permissions?.[permission])) { continue; } @@ -30,6 +31,7 @@ export function generateMenuItems(_routes: RouteConfig[], _permissions: Record { this.setState({ refreshing: false }); }, 0); } + + setLogo = (logo: string) => { + this.setState({ logo }); + }; } export default BaseModel; diff --git a/src/pages/router/dynamic/index.tsx b/src/pages/router/dynamic/index.tsx new file mode 100644 index 0000000..dca6b9a --- /dev/null +++ b/src/pages/router/dynamic/index.tsx @@ -0,0 +1,122 @@ +import router from '@/router'; +import { getRandomString } from '@/utils'; +import { Alert, Button, Card, Space } from 'antd'; +import { useTranslation } from 'react-i18next'; +import SvgIcon from '@/components/SvgIcon'; + +/** + * 动态路由 + */ +const RouterDynamic = () => { + const { t: t_router } = useTranslation('router'); + + function genNewRoute() { + const newPath = getRandomString(); + const newRoute = { + path: newPath, + component: () => import('@/pages/router/tempRoute'), + name: `临时路由-${newPath}`, + }; + return newRoute; + } + + function onClickAddTail() { + router.setSiblings((routesConfig) => { + routesConfig.push(genNewRoute()); + }); + } + + function onClickAddHead() { + router.setSiblings((routesConfig) => { + routesConfig.unshift(genNewRoute()); + }); + } + + function onClickAddMiddle(index: number) { + router.setSiblings((routesConfig) => { + routesConfig.splice(index, 0, genNewRoute()); + }); + } + + function onClickDelTail() { + router.setSiblings((routesConfig) => { + routesConfig.pop(); + }); + } + + function onClickDelHead() { + router.setSiblings((routesConfig) => { + routesConfig.shift(); + }); + } + + function onClickDelMiddle(index: number) { + router.setSiblings((routesConfig) => { + routesConfig.splice(index, 1); + }); + } + + function onClickAddAny() { + router.setSiblings('/error-page', (routesConfig) => { + routesConfig.push(genNewRoute()); + }); + } + + function onClickDelAny() { + router.setSiblings('/error-page', (routesConfig, parent) => { + parent.children = routesConfig.filter(item => { + return !item.name?.startsWith('临时路由-'); + }); + }); + } + + function onClickEdit() { + router.setItem((routesConfigItem) => { + routesConfigItem.name = t_router('被动态修改的路由'); + routesConfigItem.icon = ; + }); + } + + function onClickReset() { + router.setItem((routesConfigItem) => { + routesConfigItem.name = '动态路由'; + routesConfigItem.icon = ; + }); + } + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default RouterDynamic; diff --git a/src/pages/router/meta/index.tsx b/src/pages/router/meta/index.tsx new file mode 100644 index 0000000..12e4512 --- /dev/null +++ b/src/pages/router/meta/index.tsx @@ -0,0 +1,68 @@ +import SvgIcon from '@/components/SvgIcon'; +import { baseModel } from '@/models/base'; +import router from '@/router'; +import { Button, Card, Space } from 'antd'; +import { useTranslation } from 'react-i18next'; + +/** + * 动态meta + */ +const Meta = () => { + const { t: t_router } = useTranslation('router'); + + function onClickEditTitle() { + router.setItem((routesConfigItem) => { + routesConfigItem.name = 'rac'; + }); + } + function onClickResetTitle() { + router.setItem((routesConfigItem) => { + routesConfigItem.name = '动态meta'; + }); + } + + function onClickEditLogo() { + baseModel.setLogo('/images/react.svg'); + } + function onClickResetLogo() { + baseModel.setLogo('/images/logo.png'); + } + + function onClickEditIcon() { + router.setItem((routesConfigItem) => { + routesConfigItem.icon = ; + }); + } + function onClickResetIcon() { + router.setItem((routesConfigItem) => { + routesConfigItem.icon = null; + }); + } + + return ( +
+ + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default Meta; diff --git a/src/pages/router/tempRoute/index.tsx b/src/pages/router/tempRoute/index.tsx new file mode 100644 index 0000000..76af983 --- /dev/null +++ b/src/pages/router/tempRoute/index.tsx @@ -0,0 +1,15 @@ +import { useTranslation } from 'react-i18next'; + +/** + * 临时路由 + */ +const TempRoute = () => { + const { t: t_router } = useTranslation('router'); + return ( +
+ {t_router('我是通过router.setSiblings()方法新增的临时路由')} +
+ ); +}; + +export default TempRoute; diff --git a/src/pages/separation/index.tsx b/src/pages/separation/index.tsx new file mode 100644 index 0000000..c9c506a --- /dev/null +++ b/src/pages/separation/index.tsx @@ -0,0 +1,17 @@ +import withAuth from '@/components/business/withAuth'; +import Back from '@/components/Back'; +import { Alert, Card } from 'antd'; +import { useTranslation } from 'react-i18next'; + +const Separation = withAuth(() => { + const { t: t_menu } = useTranslation('menu'); + return ( + + + + + ); +}); + +export default Separation; + diff --git a/src/pages/singleSider/index.tsx b/src/pages/singleSider/index.tsx new file mode 100644 index 0000000..cc404b1 --- /dev/null +++ b/src/pages/singleSider/index.tsx @@ -0,0 +1,11 @@ +import { Alert } from 'antd'; +import { useTranslation } from 'react-i18next'; + +const SingleSider = () => { + const { t: t_menu } = useTranslation('menu'); + return ( + + ); +}; + +export default SingleSider; diff --git a/src/router/config/index.tsx b/src/router/config/index.tsx index 2cd8f9e..1d0b7ee 100644 --- a/src/router/config/index.tsx +++ b/src/router/config/index.tsx @@ -51,6 +51,29 @@ export const routesConfig: RouteConfig[] = [ permission: 'profile', icon: , }, + { + path: 'router', + name: '路由', + icon: , + children: [ + { + path: '', + redirect: 'dynamic', + }, + { + path: 'dynamic', + component: () => import('@/pages/router/dynamic'), + name: '动态路由', + icon: , + }, + { + path: 'meta', + component: () => import('@/pages/router/meta'), + name: '动态meta', + icon: , + }, + ], + }, { path: 'nest', component: () => import('@/pages/nest'), @@ -130,8 +153,26 @@ export const routesConfig: RouteConfig[] = [ }, ], }, + { + external: true, + path: 'https://www.baidu.com', + name: '外链', + icon: , + }, + { + path: 'singleSider', + component: () => import('@/pages/singleSider'), + name: '单栏', + icon: , + }, ], }, + { + path: '/separation', + component: () => import('@/pages/separation'), + name: '独立布局', + icon: , + }, { path: '/no-access', component: () => import('@/pages/noAccess'), diff --git a/src/utils/index.ts b/src/utils/index.ts index dcebd7a..27e4d5f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -13,3 +13,18 @@ export function isTrue(val: 0 | 1) { export function enumer(val: boolean): BlEnum { return val ? 1 : 0; } + +/** + * 获取随机字符串 + */ +export function getRandomString(options?: { + prefix?: string; + timestamp?: boolean; + length?: number; +}) { + const { prefix, timestamp, length = 6 } = options ?? {}; + const prefixChar = prefix ? `${prefix}_` : ''; + const timestampChar = timestamp ? `${new Date().getTime()}_` : ''; + const stringChar = Math.random().toString(36).slice(-length); + return `${prefixChar}${timestampChar}${stringChar}`; +}