From e42e1c01fb739289fb97824d74f32271089ba8ef Mon Sep 17 00:00:00 2001 From: wuxichen <17301714657@163.com> Date: Fri, 12 Sep 2025 15:37:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8A=A8=E6=80=81=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/defaultSettings.ts | 7 +- config/routes.ts | 21 ++++- src/app.tsx | 101 +++++++++++++++++++--- src/global.less | 8 +- src/locales/zh-CN/menu.ts | 101 +++++++++++----------- src/pages/404.tsx | 13 +-- src/pages/system/tenant/list/index.tsx | 5 ++ src/pages/system/tenant/package/index.tsx | 5 ++ src/pages/user/login/index.tsx | 2 +- src/services/login/index.ts | 1 - src/services/login/oauth2/index.ts | 76 ++++++++-------- src/services/system/menu/index.ts | 1 + src/utils/menuUtils.ts | 97 +++++++++++++++++++++ tsconfig.json | 1 + 14 files changed, 321 insertions(+), 118 deletions(-) create mode 100644 src/pages/system/tenant/list/index.tsx create mode 100644 src/pages/system/tenant/package/index.tsx create mode 100644 src/utils/menuUtils.ts diff --git a/config/defaultSettings.ts b/config/defaultSettings.ts index 04f5d81..be8e4a5 100644 --- a/config/defaultSettings.ts +++ b/config/defaultSettings.ts @@ -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 diff --git a/config/routes.ts b/config/routes.ts index ba9c580..70941b3 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -10,6 +10,7 @@ * @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn, 注意去除风格后缀和大小写,如想要配置图标为 则取值应为 stepBackward 或 StepBackward,如想要配置图标为 则取值应为 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: "*", }, ]; diff --git a/src/app.tsx b/src/app.tsx index 9b47b58..318080d 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -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 = ({ , , ], + menu: { + locale: false, + // 关闭国际化- + }, avatarProps: { src: initialState?.currentUser?.user.avatar, title: , @@ -117,17 +136,9 @@ export const layout: RunTimeLayoutConfig = ({ width: "331px", }, ], - links: isDevOrTest - ? [ - - - OpenAPI 文档 - , - ] - : [], menuHeaderRender: undefined, // 自定义 403 页面 - // unAccessible:
unAccessible
, + unAccessible:
unAccessible
, // 增加一个 loading 的状态 childrenRender: (children) => { // if (initialState?.loading) return ; @@ -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 | 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: , + }, + ...loopMenuItem(item.children, item.menuID), + ], + }, + ]; + } else { + return [ + { + path: item.path, + name: item.name, + icon: item.icon, + id: item.menuID, + parentId: pId, + element: ( + } + > + {Component && } + + ), + children: [], // 添加缺失的 children 属性 + }, + ]; + } + }); +}; diff --git a/src/global.less b/src/global.less index 1959f4f..a39376d 100644 --- a/src/global.less +++ b/src/global.less @@ -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; diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index fecb70a..33f73db 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -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": "拓扑编辑器", + // 基础设施相关菜单 }; diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 9734cc5..01d33f2 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -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 = () => ( history.push('/')}> - {useIntl().formatMessage({ id: 'pages.404.buttonText' })} + } /> diff --git a/src/pages/system/tenant/list/index.tsx b/src/pages/system/tenant/list/index.tsx new file mode 100644 index 0000000..66349a7 --- /dev/null +++ b/src/pages/system/tenant/list/index.tsx @@ -0,0 +1,5 @@ +const TenantList = () => { + return
TenantList
; +}; + +export default TenantList; diff --git a/src/pages/system/tenant/package/index.tsx b/src/pages/system/tenant/package/index.tsx new file mode 100644 index 0000000..34286af --- /dev/null +++ b/src/pages/system/tenant/package/index.tsx @@ -0,0 +1,5 @@ +const TenantPackage = () => { + return
TenantPackage
; +}; + +export default TenantPackage; diff --git a/src/pages/user/login/index.tsx b/src/pages/user/login/index.tsx index e12f08b..7c61b2b 100644 --- a/src/pages/user/login/index.tsx +++ b/src/pages/user/login/index.tsx @@ -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)", diff --git a/src/services/login/index.ts b/src/services/login/index.ts index 864eb04..a5c4315 100644 --- a/src/services/login/index.ts +++ b/src/services/login/index.ts @@ -40,7 +40,6 @@ export async function login( // }; export async function getTenantIdByName( - // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) params: { name: string }, options?: { [key: string]: any } ) { diff --git a/src/services/login/oauth2/index.ts b/src/services/login/oauth2/index.ts index f4a67fb..5062615 100644 --- a/src/services/login/oauth2/index.ts +++ b/src/services/login/oauth2/index.ts @@ -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) +// } +// }) +// } diff --git a/src/services/system/menu/index.ts b/src/services/system/menu/index.ts index e94500e..8f048ed 100644 --- a/src/services/system/menu/index.ts +++ b/src/services/system/menu/index.ts @@ -16,6 +16,7 @@ export interface MenuVO { keepAlive: boolean; alwaysShow?: boolean; createTime: Date; + children?: MenuVO[]; } // 查询菜单(精简)列表 diff --git a/src/utils/menuUtils.ts b/src/utils/menuUtils.ts new file mode 100644 index 0000000..fd62235 --- /dev/null +++ b/src/utils/menuUtils.ts @@ -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)); +} diff --git a/tsconfig.json b/tsconfig.json index 2cd5f8c..71939ed 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "baseUrl": "./", "target": "esnext", + "module": "esnext", "moduleResolution": "node", "jsx": "react-jsx", "esModuleInterop": true,