feat: 动态路由

This commit is contained in:
2025-09-12 15:37:07 +08:00
parent 2bb11b49fe
commit e42e1c01fb
14 changed files with 321 additions and 118 deletions

View File

@@ -12,13 +12,14 @@ const Settings: ProLayoutProps & {
colorPrimary: "#1890ff",
layout: "mix",
contentWidth: "Fluid",
fixedHeader: false,
fixedHeader: true,
fixSiderbar: true,
colorWeak: false,
title: "tashow - 管理后台",
title: "百业到家云控台",
pwa: true,
logo: "https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg",
logo: "/logo.svg",
iconfontUrl: "",
splitMenus: true,
token: {
// 参见ts声明demo 见文档通过token 修改样式
//https://procomponents.ant.design/components/layout#%E9%80%9A%E8%BF%87-token-%E4%BF%AE%E6%94%B9%E6%A0%B7%E5%BC%8F

View File

@@ -10,6 +10,7 @@
* @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn 注意去除风格后缀和大小写,如想要配置图标为 <StepBackwardOutlined /> 则取值应为 stepBackward 或 StepBackward如想要配置图标为 <UserOutlined /> 则取值应为 user 或者 User
* @doc https://umijs.org/docs/guides/routes
*/
export default [
{
path: "/user",
@@ -28,6 +29,24 @@ export default [
icon: "smile",
component: "./Welcome",
},
// {
// path: "/system1",
// name: "system1",
// icon: "smile",
// routes: [
// {
// name: "tenant",
// path: "/system1/tenant",
// routes: [
// {
// name: "package",
// path: "/system1/tenant/package",
// component: "system/tenant/package",
// },
// ],
// },
// ],
// },
{
path: "/admin",
name: "admin",
@@ -58,6 +77,6 @@ export default [
{
component: "404",
layout: false,
path: "./*",
path: "*",
},
];

View File

@@ -1,9 +1,9 @@
import { LinkOutlined } from "@ant-design/icons";
import React, { Children, Component, JSX, Suspense } from "react";
import { Spin } from "antd";
import type { Settings as LayoutSettings } from "@ant-design/pro-components";
import { SettingDrawer } from "@ant-design/pro-components";
import type { RequestConfig, RunTimeLayoutConfig } from "@umijs/max";
import { history, Link } from "@umijs/max";
import React from "react";
import { history, Link, Navigate } from "@umijs/max";
import {
AvatarDropdown,
AvatarName,
@@ -16,13 +16,20 @@ import type { UserVO, TokenType, UserInfoVO } from "@/services/login/types";
import defaultSettings from "../config/defaultSettings";
import { errorConfig } from "./requestErrorConfig";
import "@ant-design/v5-patch-for-react-19";
import { getAccessToken, getRefreshToken, getTenantId } from "./utils/auth";
import { getAccessToken, getRefreshToken, getTenantId } from "@/utils/auth";
import { CACHE_KEY, useCache } from "./hooks/web/useCache";
import { MenuVO } from "./services/system/menu";
import {
transformBackendMenuToFlatRoutes,
transformMenuToRoutes,
} from "@/utils/menuUtils";
const isDev = process.env.NODE_ENV === "development";
const isDevOrTest = isDev || process.env.CI;
const loginPath = "/user/login";
// 全局存储菜单数据和路由映射
/**
* @see https://umijs.org/docs/api/runtime-config#getinitialstate
* */
@@ -43,6 +50,9 @@ export async function getInitialState(): Promise<{
const { data } = await getInfo();
wsCache.set(CACHE_KEY.USER, data);
wsCache.set(CACHE_KEY.ROLE_ROUTERS, data.menus);
// 转换菜单格式
return data;
} catch (_error) {
history.push(loginPath);
@@ -51,12 +61,17 @@ export async function getInitialState(): Promise<{
};
// 如果不是登录页面,执行
const { location } = history;
if (
![loginPath, "/user/register", "/user/register-result"].includes(
location.pathname
)
) {
const currentUser = await fetchUserInfo();
const currentUser = wsCache.get(CACHE_KEY.USER);
if (getAccessToken() && !currentUser) {
fetchUserInfo();
}
return {
fetchUserInfo,
currentUser,
@@ -79,6 +94,10 @@ export const layout: RunTimeLayoutConfig = ({
<Question key="doc" />,
<SelectLang key="SelectLang" />,
],
menu: {
locale: false,
// 关闭国际化-
},
avatarProps: {
src: initialState?.currentUser?.user.avatar,
title: <AvatarName />,
@@ -117,17 +136,9 @@ export const layout: RunTimeLayoutConfig = ({
width: "331px",
},
],
links: isDevOrTest
? [
<Link key="openapi" to="/umi/plugin/openapi" target="_blank">
<LinkOutlined />
<span>OpenAPI </span>
</Link>,
]
: [],
menuHeaderRender: undefined,
// 自定义 403 页面
// unAccessible: <div>unAccessible</div>,
unAccessible: <div>unAccessible</div>,
// 增加一个 loading 的状态
childrenRender: (children) => {
// if (initialState?.loading) return <PageLoading />;
@@ -187,3 +198,65 @@ export const request: RequestConfig = {
},
],
};
// umi 4 使用 modifyRoutes
export function patchClientRoutes({ routes }: { routes: any }) {
const { wsCache } = useCache();
const globalMenus = wsCache.get(CACHE_KEY.ROLE_ROUTERS);
const routerIndex = routes.findIndex((item: any) => item.path === "/");
const parentId = routes[routerIndex].id;
if (globalMenus) {
routes[routerIndex]["routes"].push(...loopMenuItem(globalMenus, parentId));
}
}
const loopMenuItem = (menus: any[], pId: number | string): any[] => {
return menus.flatMap((item) => {
let Component: React.ComponentType<any> | null = null;
if (item.component && item.component.length > 0) {
// 防止配置了路由,但本地暂未添加对应的页面,产生的错误
console.log(item.component);
Component = React.lazy(() => {
const importComponent = () => import(`@/pages/${item.component}`);
const import404 = () => import("@/pages/404");
return importComponent().catch(import404);
});
}
if (item.children && item.children.length > 0) {
return [
{
path: item.path,
name: item.name,
icon: item.icon,
id: item.id,
parentId: pId,
children: [
{
path: item.url,
element: <Navigate to={item.children[0].url} replace />,
},
...loopMenuItem(item.children, item.menuID),
],
},
];
} else {
return [
{
path: item.path,
name: item.name,
icon: item.icon,
id: item.menuID,
parentId: pId,
element: (
<React.Suspense
fallback={<Spin style={{ width: "100%", height: "100%" }} />}
>
{Component && <Component />}
</React.Suspense>
),
children: [], // 添加缺失的 children 属性
},
];
}
});
};

View File

@@ -45,9 +45,9 @@ body,
height: 100%;
margin: 0;
padding: 0;
font-family:
AlibabaSans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-family: AlibabaSans, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
.colorWeak {
@@ -55,7 +55,7 @@ body,
}
.ant-layout {
min-height: 100vh;
min-height: 100vh !important;
}
.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
left: unset;

View File

@@ -1,52 +1,53 @@
export default {
'menu.welcome': '欢迎',
'menu.more-blocks': '更多区块',
'menu.home': '首页',
'menu.admin': '管理页',
'menu.admin.sub-page': '二级管理页',
'menu.login': '登录',
'menu.register': '注册',
'menu.register-result': '注册结果',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': '分析页',
'menu.dashboard.monitor': '监控页',
'menu.dashboard.workplace': '工作台',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.form': '表单页',
'menu.form.basic-form': '基础表单',
'menu.form.step-form': '分步表单',
'menu.form.step-form.info': '分步表单(填写转账信息)',
'menu.form.step-form.confirm': '分步表单(确认转账信息)',
'menu.form.step-form.result': '分步表单(完成)',
'menu.form.advanced-form': '高级表单',
'menu.list': '列表页',
'menu.list.table-list': '查询表格',
'menu.list.basic-list': '标准列表',
'menu.list.card-list': '卡片列表',
'menu.list.search-list': '搜索列表',
'menu.list.search-list.articles': '搜索列表(文章)',
'menu.list.search-list.projects': '搜索列表(项目)',
'menu.list.search-list.applications': '搜索列表(应用)',
'menu.profile': '详情页',
'menu.profile.basic': '基础详情页',
'menu.profile.advanced': '高级详情页',
'menu.result': '结果页',
'menu.result.success': '成功页',
'menu.result.fail': '失败页',
'menu.exception': '异常页',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': '触发错误',
'menu.account': '个人页',
'menu.account.center': '个人中心',
'menu.account.settings': '个人设置',
'menu.account.trigger': '触发报错',
'menu.account.logout': '退出登录',
'menu.editor': '图形编辑器',
'menu.editor.flow': '流程编辑器',
'menu.editor.mind': '脑图编辑器',
'menu.editor.koni': '拓扑编辑器',
"menu.welcome": "欢迎",
"menu.more-blocks": "更多区块",
"menu.home": "首页",
"menu.admin": "管理页",
"menu.admin.sub-page": "二级管理页",
"menu.login": "登录",
"menu.register": "注册",
"menu.register-result": "注册结果",
"menu.dashboard": "Dashboard",
"menu.dashboard.analysis": "分析页",
"menu.dashboard.monitor": "监控页",
"menu.dashboard.workplace": "工作台",
"menu.exception.403": "403",
"menu.exception.404": "404",
"menu.exception.500": "500",
"menu.form": "表单页",
"menu.form.basic-form": "基础表单",
"menu.form.step-form": "分步表单",
"menu.form.step-form.info": "分步表单(填写转账信息)",
"menu.form.step-form.confirm": "分步表单(确认转账信息)",
"menu.form.step-form.result": "分步表单(完成)",
"menu.form.advanced-form": "高级表单",
"menu.list": "列表页",
"menu.list.table-list": "查询表格",
"menu.list.basic-list": "标准列表",
"menu.list.card-list": "卡片列表",
"menu.list.search-list": "搜索列表",
"menu.list.search-list.articles": "搜索列表(文章)",
"menu.list.search-list.projects": "搜索列表(项目)",
"menu.list.search-list.applications": "搜索列表(应用)",
"menu.profile": "详情页",
"menu.profile.basic": "基础详情页",
"menu.profile.advanced": "高级详情页",
"menu.result": "结果页",
"menu.result.success": "成功页",
"menu.result.fail": "失败页",
"menu.exception": "异常页",
"menu.exception.not-permission": "403",
"menu.exception.not-find": "404",
"menu.exception.server-error": "500",
"menu.exception.trigger": "触发错误",
"menu.account": "个人页",
"menu.account.center": "个人中心",
"menu.account.settings": "个人设置",
"menu.account.trigger": "触发报错",
"menu.account.logout": "退出登录",
"menu.editor": "图形编辑器",
"menu.editor.flow": "流程编辑器",
"menu.editor.mind": "脑图编辑器",
"menu.editor.koni": "拓扑编辑器",
// 基础设施相关菜单
};

View File

@@ -1,16 +1,17 @@
import { history, useIntl } from '@umijs/max';
import { Button, Card, Result } from 'antd';
import React from 'react';
import { PageContainer } from "@ant-design/pro-components";
import { history, useIntl } from "@umijs/max";
import { Button, Card, Result } from "antd";
import React from "react";
const NoFoundPage: React.FC = () => (
<Card variant="borderless">
<Result
status="404"
title="404"
subTitle={useIntl().formatMessage({ id: 'pages.404.subTitle' })}
subTitle={useIntl().formatMessage({ id: "pages.404.subTitle" })}
extra={
<Button type="primary" onClick={() => history.push('/')}>
{useIntl().formatMessage({ id: 'pages.404.buttonText' })}
<Button type="primary" onClick={() => history.push("/")}>
{useIntl().formatMessage({ id: "pages.404.buttonText" })}
</Button>
}
/>

View File

@@ -0,0 +1,5 @@
const TenantList = () => {
return <div>TenantList</div>;
};
export default TenantList;

View File

@@ -0,0 +1,5 @@
const TenantPackage = () => {
return <div>TenantPackage</div>;
};
export default TenantPackage;

View File

@@ -104,7 +104,7 @@ const Page = () => {
backdropFilter: "blur(4px)",
}}
onFinish={handleSubmit}
subTitle="百业到家-管理平台"
subTitle="百业到家云控台"
// activityConfig={{
// style: {
// boxShadow: "0px 0px 8px rgba(0, 0, 0, 0.2)",

View File

@@ -40,7 +40,6 @@ export async function login(
// };
export async function getTenantIdByName(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: { name: string },
options?: { [key: string]: any }
) {

View File

@@ -1,41 +1,41 @@
import request from '@/config/axios'
import { request } from "@umijs/max";
// 获得授权信息
export const getAuthorize = (clientId: string) => {
return request.get({ url: '/system/oauth2/authorize?clientId=' + clientId })
}
// export const getAuthorize = (clientId: string) => {
// return request.get({ url: '/system/oauth2/authorize?clientId=' + clientId })
// }
// 发起授权
export const authorize = (
responseType: string,
clientId: string,
redirectUri: string,
state: string,
autoApprove: boolean,
checkedScopes: string[],
uncheckedScopes: string[]
) => {
// 构建 scopes
const scopes = {}
for (const scope of checkedScopes) {
scopes[scope] = true
}
for (const scope of uncheckedScopes) {
scopes[scope] = false
}
// 发起请求
return request.post({
url: '/system/oauth2/authorize',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
params: {
response_type: responseType,
client_id: clientId,
redirect_uri: redirectUri,
state: state,
auto_approve: autoApprove,
scope: JSON.stringify(scopes)
}
})
}
// // 发起授权
// export const authorize = (
// responseType: string,
// clientId: string,
// redirectUri: string,
// state: string,
// autoApprove: boolean,
// checkedScopes: string[],
// uncheckedScopes: string[]
// ) => {
// // 构建 scopes
// const scopes = {}
// for (const scope of checkedScopes) {
// scopes[scope] = true
// }
// for (const scope of uncheckedScopes) {
// scopes[scope] = false
// }
// // 发起请求
// return request.post({
// url: '/system/oauth2/authorize',
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded'
// },
// params: {
// response_type: responseType,
// client_id: clientId,
// redirect_uri: redirectUri,
// state: state,
// auto_approve: autoApprove,
// scope: JSON.stringify(scopes)
// }
// })
// }

View File

@@ -16,6 +16,7 @@ export interface MenuVO {
keepAlive: boolean;
alwaysShow?: boolean;
createTime: Date;
children?: MenuVO[];
}
// 查询菜单(精简)列表

97
src/utils/menuUtils.ts Normal file
View File

@@ -0,0 +1,97 @@
// src/utils/menuUtils.ts
import { MenuVO } from "@/services/system/menu";
import type { MenuDataItem } from "@ant-design/pro-components";
// src/utils/menuUtils.ts
// src/utils/menuUtils.ts
// src/utils/route.ts
export function transformMenuToRoutes(menuData: MenuVO[]): any[] {
return menuData.map((item) => ({
path: item.path,
name: item.name,
icon: item.icon,
component: item.component,
routes: item.children ? transformMenuToRoutes(item.children) : undefined,
}));
}
// src/utils/route.ts
export function transformBackendMenuToFlatRoutes(menuData: any[]) {
const flatRoutes: any[] = [];
function processMenu(items: any[], parentRouteId = "ant-design-pro-layout") {
items.forEach((item) => {
const currentRouteId = `route-${item.id}`;
// 处理路径 - 如果是子路由,需要组合完整路径
let fullPath = item.path;
if (item.parentId !== 0 && !item.path.startsWith("/")) {
// 子路由需要相对路径
fullPath = item.path;
}
const route: any = {
id: currentRouteId,
path: fullPath,
name: item.name,
parentId: parentRouteId,
};
// 添加图标(如果不是 # 的话)
if (item.icon && item.icon !== "#") {
route.icon = item.icon;
}
// 添加组件路径
if (item.component) {
// 转换组件路径为动态导入格式
route.component = item.component;
}
// 其他属性
if (!item.visible) {
route.hideInMenu = true;
}
flatRoutes.push(route);
// 递归处理子菜单
if (item.children && item.children.length > 0) {
processMenu(item.children, currentRouteId);
}
});
}
processMenu(menuData);
return flatRoutes;
}
export function transformMenuData(menuData: any[]) {
const transformItem = (item: any, parentPath = "") => {
const fullPath = item.path.startsWith("/")
? item.path
: `${parentPath}/${item.path}`;
const result: any = {
path: fullPath,
name: item.name,
key: `${item.id}`,
};
if (item.icon && item.icon !== "#") {
result.icon = item.icon;
}
if (!item.visible) {
result.hideInMenu = true;
}
if (item.children && item.children.length > 0) {
result.children = item.children.map((child: any) =>
transformItem(child, fullPath)
);
}
return;
result;
};
return menuData.map((item) => transformItem(item));
}

View File

@@ -2,6 +2,7 @@
"compilerOptions": {
"baseUrl": "./",
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"jsx": "react-jsx",
"esModuleInterop": true,