diff --git a/apps/admin/src/layout/feature/components/iconify-icon/index.tsx b/apps/admin/src/components/iconify-icon/index.tsx similarity index 100% rename from apps/admin/src/layout/feature/components/iconify-icon/index.tsx rename to apps/admin/src/components/iconify-icon/index.tsx diff --git a/apps/admin/src/layout/feature/components/iconify-icon/style.ts b/apps/admin/src/components/iconify-icon/style.ts similarity index 100% rename from apps/admin/src/layout/feature/components/iconify-icon/style.ts rename to apps/admin/src/components/iconify-icon/style.ts diff --git a/apps/admin/src/hooks/web/useKeepAlive.ts b/apps/admin/src/hooks/web/useKeepAlive.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/admin/src/layout/multi-tab/index.tsx b/apps/admin/src/layout/multi-tab/index.tsx new file mode 100644 index 00000000..35ca4a48 --- /dev/null +++ b/apps/admin/src/layout/multi-tab/index.tsx @@ -0,0 +1,46 @@ +import { Dropdown, type MenuProps, Tabs, TabsProps } from 'antd'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import IconifyIcon from '@/components/iconify-icon'; + +import { MultiTabOperation, ThemeLayout } from '#/enum'; + +type Props = { + offsetTop?: boolean; +}; + +export default function MultiTabs({ offsetTop = true }: Props) { + const { TabPane } = Tabs; + const { t } = useTranslation(); + + const menuItens = useMemo(() => { + return [ + { + label: t('刷新'), + key: MultiTabOperation.REFRESH, + icon: , + }, + { + label: t('关闭右侧'), + key: MultiTabOperation.CLOSERIGHT, + icon: , + }, + { + label: t('关闭左侧'), + key: MultiTabOperation.CLOSELEFT, + icon: , + }, + { + label: t('关闭其他'), + key: MultiTabOperation.CLOSEOTHERS, + icon: , + }, + { + label: t('关闭全部'), + key: MultiTabOperation.CLOSEALL, + icon: , + }, + ]; + }, []); +} diff --git a/apps/admin/src/layout/multi-tab/style.ts b/apps/admin/src/layout/multi-tab/style.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/admin/src/router/hooks/index.ts b/apps/admin/src/router/hooks/index.ts new file mode 100644 index 00000000..28f26dba --- /dev/null +++ b/apps/admin/src/router/hooks/index.ts @@ -0,0 +1 @@ +export { useMatchRouteMeta } from './use-match-route-meta'; diff --git a/apps/admin/src/router/hooks/use-flattened-routes.ts b/apps/admin/src/router/hooks/use-flattened-routes.ts new file mode 100644 index 00000000..819d7638 --- /dev/null +++ b/apps/admin/src/router/hooks/use-flattened-routes.ts @@ -0,0 +1,17 @@ +import { useCallback, useMemo } from 'react'; + +import { flattenMenuRoutes, menuFilter } from '../utils'; +import { usePermissionRoutes } from './use-permission-routes'; + +/** + * 返回拍平后到菜单路由 + */ + +export function useFlattenedRoutes() { + const flattenRoutes = useCallback(flattenMenuRoutes, []); + const permissionRoutes = usePermissionRoutes(); + return useMemo(() => { + const menuRoutes = menuFilter(permissionRoutes); + return flattenRoutes(menuRoutes); + }, [flattenRoutes, permissionRoutes]); +} diff --git a/apps/admin/src/router/hooks/use-match-route-meta.tsx b/apps/admin/src/router/hooks/use-match-route-meta.tsx new file mode 100644 index 00000000..78271965 --- /dev/null +++ b/apps/admin/src/router/hooks/use-match-route-meta.tsx @@ -0,0 +1,34 @@ +import { useEffect, useState } from 'react'; +import { useMatches, useOutlet } from 'react-router-dom'; + +import { useFlattenedRoutes } from './use-flattened-routes'; +import { useRouter } from './use-router'; + +import type { RouteMeta } from '#/router'; + +export function useMatchRouteMeta() { + const { VITE_APP_HOMEPAGE: HOMEPAGE } = import.meta.env; + const [matchRouteMEta, setMatchRouteMeta] = useState(); + // 获取路由组件实例 + const children = useOutlet(); + // 获取所有匹配到路由 + const matches = useMatches(); + // 获取拍平后到路由菜单 + const flattenedRoutes = useFlattenedRoutes(); + // const pathname = usePathname(); + const { path } = useRouter(); + + useEffect(() => { + const lastRoute = matches.at(-1); + + const currentRouteMeta = flattenedRoutes.find( + (item) => `${item.key}/` === lastRoute?.pathname || item.key === lastRoute?.pathname, + ); + if (currentRouteMeta) { + if (!currentRouteMeta.hideTab) { + currentRouteMeta.outlet = children; + setMatchRouteMeta(currentRouteMeta); + } + } + }, [matches]); +} diff --git a/apps/admin/src/router/hooks/use-permission-routes.tsx b/apps/admin/src/router/hooks/use-permission-routes.tsx new file mode 100644 index 00000000..e69de29b diff --git a/apps/admin/src/router/hooks/use-router.ts b/apps/admin/src/router/hooks/use-router.ts new file mode 100644 index 00000000..bc99bf2f --- /dev/null +++ b/apps/admin/src/router/hooks/use-router.ts @@ -0,0 +1,19 @@ +import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; + +export function useRouter() { + const navigate = useNavigate(); + + const router = useMemo( + () => ({ + back: () => navigate(-1), + forward: () => navigate(1), + reload: () => window.location.reload(), + push: (href: string) => navigate(href), + replace: (href: string) => navigate(href, { replace: true }), + }), + [navigate], + ); + + return router; +} diff --git a/apps/admin/src/router/utils/index.ts b/apps/admin/src/router/utils/index.ts new file mode 100644 index 00000000..46f54465 --- /dev/null +++ b/apps/admin/src/router/utils/index.ts @@ -0,0 +1,37 @@ +import { ascend } from 'ramda'; + +import type { AppRouteObject, RouteMeta } from '#/router'; + +/** + * 根据特定条件过滤菜单项并对过滤后的项目进行排序 + * + * @param {AppRouteObject[]} items - The array of items to be filtered and sorted + * @return {AppRouteObject[]} The filtered and sorted array of items + */ +export const menuFilter = (items: AppRouteObject[]) => { + return items + .filter((item) => { + const show = item.meta?.key; + if (show && item.children) { + // eslint-disable-next-line no-param-reassign + item.children = menuFilter(item.children); + } + return show; + }) + .sort(ascend((item) => item.order || Infinity)); +}; + +/** + * 将菜单路由扁平化为 RouteMeta 对象的单个数组。 + * + * @param {AppRouteObject[]} routes - an array of AppRouteObject representing the menu routes + * @return {RouteMeta[]} a single array of RouteMeta objects + */ +export function flattenMenuRoutes(routes: AppRouteObject[]) { + return routes.reduce((prev, item) => { + const { meta, children } = item; + if (meta) prev.push(meta); + if (children) prev.push(...flattenMenuRoutes(children)); + return prev; + }, []); +} diff --git a/apps/admin/types/enum.ts b/apps/admin/types/enum.ts index a975779f..daedfcbb 100644 --- a/apps/admin/types/enum.ts +++ b/apps/admin/types/enum.ts @@ -35,3 +35,12 @@ export enum ThemeColorPresets { Orange = 'orange', Red = 'red', } + +export enum MultiTabOperation { + REFRESH = 'refresh', + CLOSE = 'close', + CLOSEOTHERS = 'closeOthers', + CLOSEALL = 'closeAll', + CLOSELEFT = 'closeLeft', + CLOSERIGHT = 'closeRight', +} diff --git a/apps/admin/types/router.ts b/apps/admin/types/router.ts new file mode 100644 index 00000000..f59096e6 --- /dev/null +++ b/apps/admin/types/router.ts @@ -0,0 +1,50 @@ +import type { ReactNode } from 'react'; +import type { RouteObject } from 'react-router-dom'; + +export interface RouteMeta { + /** + * antd menu selectedKeys + */ + key: string; + /** + * menu label, i18n + */ + label: string; + /** + * menu prefix icon + */ + icon?: ReactNode; + /** + * menu suffix icon + */ + suffix?: ReactNode; + /** + * hide in menu + */ + hideMenu?: boolean; + /** + * hide in multi tab + */ + hideTab?: boolean; + /** + * disable in menu + */ + disabled?: boolean; + /** + * react router outlet + */ + outlet?: any; + /** + * use to refresh tab + */ + timeStamp?: string; + /** + * external link and iframe need + */ + frameSrc?: string; +} +export type AppRouteObject = { + order?: number; + meta?: RouteMeta; + children?: AppRouteObject[]; +} & Omit;