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", colorPrimary: "#1890ff",
layout: "mix", layout: "mix",
contentWidth: "Fluid", contentWidth: "Fluid",
fixedHeader: false, fixedHeader: true,
fixSiderbar: true, fixSiderbar: true,
colorWeak: false, colorWeak: false,
title: "tashow - 管理后台", title: "百业到家云控台",
pwa: true, pwa: true,
logo: "https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg", logo: "/logo.svg",
iconfontUrl: "", iconfontUrl: "",
splitMenus: true,
token: { token: {
// 参见ts声明demo 见文档通过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 //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 * @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn 注意去除风格后缀和大小写,如想要配置图标为 <StepBackwardOutlined /> 则取值应为 stepBackward 或 StepBackward如想要配置图标为 <UserOutlined /> 则取值应为 user 或者 User
* @doc https://umijs.org/docs/guides/routes * @doc https://umijs.org/docs/guides/routes
*/ */
export default [ export default [
{ {
path: "/user", path: "/user",
@@ -28,6 +29,24 @@ export default [
icon: "smile", icon: "smile",
component: "./Welcome", 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", path: "/admin",
name: "admin", name: "admin",
@@ -58,6 +77,6 @@ export default [
{ {
component: "404", component: "404",
layout: false, 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 type { Settings as LayoutSettings } from "@ant-design/pro-components";
import { SettingDrawer } from "@ant-design/pro-components"; import { SettingDrawer } from "@ant-design/pro-components";
import type { RequestConfig, RunTimeLayoutConfig } from "@umijs/max"; import type { RequestConfig, RunTimeLayoutConfig } from "@umijs/max";
import { history, Link } from "@umijs/max"; import { history, Link, Navigate } from "@umijs/max";
import React from "react";
import { import {
AvatarDropdown, AvatarDropdown,
AvatarName, AvatarName,
@@ -16,13 +16,20 @@ import type { UserVO, TokenType, UserInfoVO } from "@/services/login/types";
import defaultSettings from "../config/defaultSettings"; import defaultSettings from "../config/defaultSettings";
import { errorConfig } from "./requestErrorConfig"; import { errorConfig } from "./requestErrorConfig";
import "@ant-design/v5-patch-for-react-19"; 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 { 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 isDev = process.env.NODE_ENV === "development";
const isDevOrTest = isDev || process.env.CI; const isDevOrTest = isDev || process.env.CI;
const loginPath = "/user/login"; const loginPath = "/user/login";
// 全局存储菜单数据和路由映射
/** /**
* @see https://umijs.org/docs/api/runtime-config#getinitialstate * @see https://umijs.org/docs/api/runtime-config#getinitialstate
* */ * */
@@ -43,6 +50,9 @@ export async function getInitialState(): Promise<{
const { data } = await getInfo(); const { data } = await getInfo();
wsCache.set(CACHE_KEY.USER, data); wsCache.set(CACHE_KEY.USER, data);
wsCache.set(CACHE_KEY.ROLE_ROUTERS, data.menus); wsCache.set(CACHE_KEY.ROLE_ROUTERS, data.menus);
// 转换菜单格式
return data; return data;
} catch (_error) { } catch (_error) {
history.push(loginPath); history.push(loginPath);
@@ -51,12 +61,17 @@ export async function getInitialState(): Promise<{
}; };
// 如果不是登录页面,执行 // 如果不是登录页面,执行
const { location } = history; const { location } = history;
if ( if (
![loginPath, "/user/register", "/user/register-result"].includes( ![loginPath, "/user/register", "/user/register-result"].includes(
location.pathname location.pathname
) )
) { ) {
const currentUser = await fetchUserInfo(); const currentUser = wsCache.get(CACHE_KEY.USER);
if (getAccessToken() && !currentUser) {
fetchUserInfo();
}
return { return {
fetchUserInfo, fetchUserInfo,
currentUser, currentUser,
@@ -79,6 +94,10 @@ export const layout: RunTimeLayoutConfig = ({
<Question key="doc" />, <Question key="doc" />,
<SelectLang key="SelectLang" />, <SelectLang key="SelectLang" />,
], ],
menu: {
locale: false,
// 关闭国际化-
},
avatarProps: { avatarProps: {
src: initialState?.currentUser?.user.avatar, src: initialState?.currentUser?.user.avatar,
title: <AvatarName />, title: <AvatarName />,
@@ -117,17 +136,9 @@ export const layout: RunTimeLayoutConfig = ({
width: "331px", width: "331px",
}, },
], ],
links: isDevOrTest
? [
<Link key="openapi" to="/umi/plugin/openapi" target="_blank">
<LinkOutlined />
<span>OpenAPI </span>
</Link>,
]
: [],
menuHeaderRender: undefined, menuHeaderRender: undefined,
// 自定义 403 页面 // 自定义 403 页面
// unAccessible: <div>unAccessible</div>, unAccessible: <div>unAccessible</div>,
// 增加一个 loading 的状态 // 增加一个 loading 的状态
childrenRender: (children) => { childrenRender: (children) => {
// if (initialState?.loading) return <PageLoading />; // 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%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: font-family: AlibabaSans, -apple-system, BlinkMacSystemFont, "Segoe UI",
AlibabaSans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
} }
.colorWeak { .colorWeak {
@@ -55,7 +55,7 @@ body,
} }
.ant-layout { .ant-layout {
min-height: 100vh; min-height: 100vh !important;
} }
.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed { .ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
left: unset; left: unset;

View File

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

View File

@@ -1,16 +1,17 @@
import { history, useIntl } from '@umijs/max'; import { PageContainer } from "@ant-design/pro-components";
import { Button, Card, Result } from 'antd'; import { history, useIntl } from "@umijs/max";
import React from 'react'; import { Button, Card, Result } from "antd";
import React from "react";
const NoFoundPage: React.FC = () => ( const NoFoundPage: React.FC = () => (
<Card variant="borderless"> <Card variant="borderless">
<Result <Result
status="404" status="404"
title="404" title="404"
subTitle={useIntl().formatMessage({ id: 'pages.404.subTitle' })} subTitle={useIntl().formatMessage({ id: "pages.404.subTitle" })}
extra={ extra={
<Button type="primary" onClick={() => history.push('/')}> <Button type="primary" onClick={() => history.push("/")}>
{useIntl().formatMessage({ id: 'pages.404.buttonText' })} {useIntl().formatMessage({ id: "pages.404.buttonText" })}
</Button> </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)", backdropFilter: "blur(4px)",
}} }}
onFinish={handleSubmit} onFinish={handleSubmit}
subTitle="百业到家-管理平台" subTitle="百业到家云控台"
// activityConfig={{ // activityConfig={{
// style: { // style: {
// boxShadow: "0px 0px 8px rgba(0, 0, 0, 0.2)", // boxShadow: "0px 0px 8px rgba(0, 0, 0, 0.2)",

View File

@@ -40,7 +40,6 @@ export async function login(
// }; // };
export async function getTenantIdByName( export async function getTenantIdByName(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: { name: string }, params: { name: string },
options?: { [key: string]: any } 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) => { // export const getAuthorize = (clientId: string) => {
return request.get({ url: '/system/oauth2/authorize?clientId=' + clientId }) // return request.get({ url: '/system/oauth2/authorize?clientId=' + clientId })
} // }
// 发起授权 // // 发起授权
export const authorize = ( // export const authorize = (
responseType: string, // responseType: string,
clientId: string, // clientId: string,
redirectUri: string, // redirectUri: string,
state: string, // state: string,
autoApprove: boolean, // autoApprove: boolean,
checkedScopes: string[], // checkedScopes: string[],
uncheckedScopes: string[] // uncheckedScopes: string[]
) => { // ) => {
// 构建 scopes // // 构建 scopes
const scopes = {} // const scopes = {}
for (const scope of checkedScopes) { // for (const scope of checkedScopes) {
scopes[scope] = true // scopes[scope] = true
} // }
for (const scope of uncheckedScopes) { // for (const scope of uncheckedScopes) {
scopes[scope] = false // scopes[scope] = false
} // }
// 发起请求 // // 发起请求
return request.post({ // return request.post({
url: '/system/oauth2/authorize', // url: '/system/oauth2/authorize',
headers: { // headers: {
'Content-Type': 'application/x-www-form-urlencoded' // 'Content-Type': 'application/x-www-form-urlencoded'
}, // },
params: { // params: {
response_type: responseType, // response_type: responseType,
client_id: clientId, // client_id: clientId,
redirect_uri: redirectUri, // redirect_uri: redirectUri,
state: state, // state: state,
auto_approve: autoApprove, // auto_approve: autoApprove,
scope: JSON.stringify(scopes) // scope: JSON.stringify(scopes)
} // }
}) // })
} // }

View File

@@ -16,6 +16,7 @@ export interface MenuVO {
keepAlive: boolean; keepAlive: boolean;
alwaysShow?: boolean; alwaysShow?: boolean;
createTime: Date; 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": { "compilerOptions": {
"baseUrl": "./", "baseUrl": "./",
"target": "esnext", "target": "esnext",
"module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"jsx": "react-jsx", "jsx": "react-jsx",
"esModuleInterop": true, "esModuleInterop": true,