From 010a96db199a3bc600ea76f70e84f83bd0a7d1a6 Mon Sep 17 00:00:00 2001 From: Yvan <8574526@qq,com> Date: Thu, 8 Jan 2026 15:06:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/config/routes.ts | 230 +++++------------ web/src/app.tsx | 187 +++++++++++--- web/src/components/BottomInfo/BottomInfo.tsx | 39 +-- web/src/locales/en-US/menu.ts | 84 ++++--- web/src/locales/zh-CN/menu.ts | 113 +++++---- web/src/pages/system/state/index.tsx | 134 ++++++++++ web/src/pages/systemTools/autoPkg/index.tsx | 203 +++++++++++++++ web/src/pages/user/login/index.tsx | 35 ++- web/src/services/kratos/initdb.ts | 5 +- web/src/utils/dynamicRouter.ts | 252 +++++++++++++++++++ 10 files changed, 974 insertions(+), 308 deletions(-) create mode 100644 web/src/pages/system/state/index.tsx create mode 100644 web/src/pages/systemTools/autoPkg/index.tsx create mode 100644 web/src/utils/dynamicRouter.ts diff --git a/web/config/routes.ts b/web/config/routes.ts index 83f5e17..4a3a7a7 100644 --- a/web/config/routes.ts +++ b/web/config/routes.ts @@ -1,8 +1,7 @@ /** * KRA - 路由配置 - * 对应 GVA 的路由结构 - * @name umi 的路由配置 - * @doc https://umijs.org/docs/guides/routes + * 路由在编译时静态配置,菜单从后端动态获取 + * 菜单显示由 app.tsx 中的 menu.request 控制 */ export default [ // 用户相关(无布局) @@ -11,7 +10,7 @@ export default [ layout: false, routes: [ { - name: 'login', + name: '登录', path: '/user/login', component: './user/login', }, @@ -21,10 +20,19 @@ export default [ // 数据库初始化(无布局) { path: '/init', + name: '初始化', layout: false, component: './init', }, + // 扫码上传(无布局,客户端页面) + { + path: '/scanUpload', + name: '扫码上传', + layout: false, + component: './example/upload/scanUpload', + }, + // 错误页面(无布局) { path: '/error', @@ -37,197 +45,84 @@ export default [ ], }, + // ========== 以下路由在菜单中由后端控制显示 ========== + // 路由必须静态配置,菜单显示由后端 /menu/getMenu 接口控制 + // 仪表盘 { path: '/dashboard', - name: 'dashboard', - icon: 'dashboard', + name: '仪表盘', component: './dashboard', + hideInMenu: true, // 菜单由后端控制 }, - // 超级管理员 - 用户管理 + // 超级管理员 { path: '/admin', - name: 'superAdmin', - icon: 'crown', - access: 'isAdmin', + name: '超级管理员', + hideInMenu: true, routes: [ - { - path: '/admin/user', - name: 'user', - icon: 'user', - component: './system/user', - }, - { - path: '/admin/authority', - name: 'authority', - icon: 'team', - component: './system/authority', - }, - { - path: '/admin/menu', - name: 'menu', - icon: 'menu', - component: './system/menu', - }, - { - path: '/admin/api', - name: 'api', - icon: 'api', - component: './system/api', - }, - { - path: '/admin/operation', - name: 'operation', - icon: 'fileSearch', - component: './system/operation', - }, - { - path: '/admin/dictionary', - name: 'dictionary', - icon: 'book', - component: './system/dictionary', - }, - { - path: '/admin/dictionary/detail/:id', - name: 'dictionaryDetail', - component: './system/dictionary/detail', - hideInMenu: true, - }, - { - path: '/admin/params', - name: 'params', - icon: 'setting', - component: './system/params', - }, + { path: '/admin/user', name: '用户管理', component: './system/user' }, + { path: '/admin/authority', name: '角色管理', component: './system/authority' }, + { path: '/admin/menu', name: '菜单管理', component: './system/menu' }, + { path: '/admin/api', name: 'API管理', component: './system/api' }, + { path: '/admin/operation', name: '操作历史', component: './system/operation' }, + { path: '/admin/dictionary', name: '字典管理', component: './system/dictionary' }, + { path: '/admin/dictionary/detail/:id', name: '字典详情', component: './system/dictionary/detail', hideInMenu: true }, + { path: '/admin/sysParams', name: '参数管理', component: './system/params' }, ], }, - // 系统工具 { path: '/systemTools', - name: 'systemTools', - icon: 'tool', - access: 'isAdmin', + name: '系统工具', + hideInMenu: true, routes: [ - { - path: '/systemTools/system', - name: 'system', - icon: 'setting', - component: './systemTools/system', - }, - { - path: '/systemTools/version', - name: 'version', - icon: 'info-circle', - component: './systemTools/version', - }, - { - path: '/systemTools/sysError', - name: 'sysError', - icon: 'warning', - component: './systemTools/sysError', - }, - { - path: '/systemTools/autoCode', - name: 'autoCode', - icon: 'code', - component: './systemTools/autoCode', - }, - { - path: '/systemTools/autoCodeAdmin', - name: 'autoCodeAdmin', - icon: 'database', - component: './systemTools/autoCodeAdmin', - }, - { - path: '/systemTools/exportTemplate', - name: 'exportTemplate', - icon: 'export', - component: './systemTools/exportTemplate', - }, - { - path: '/systemTools/formCreate', - name: 'formCreate', - icon: 'form', - component: './systemTools/formCreate', - }, - { - path: '/systemTools/installPlugin', - name: 'installPlugin', - icon: 'appstore-add', - component: './systemTools/installPlugin', - }, - { - path: '/systemTools/pubPlug', - name: 'pubPlug', - icon: 'cloud-upload', - component: './systemTools/pubPlug', - }, + { path: '/systemTools/autoCode', name: '代码生成器', component: './systemTools/autoCode' }, + { path: '/systemTools/autoCodeAdmin', name: '自动化代码管理', component: './systemTools/autoCodeAdmin' }, + { path: '/systemTools/autoCodeEdit/:id', name: '自动化代码编辑', component: './systemTools/autoCode', hideInMenu: true }, + { path: '/systemTools/formCreate', name: '表单生成器', component: './systemTools/formCreate' }, + { path: '/systemTools/system', name: '系统配置', component: './systemTools/system' }, + { path: '/systemTools/autoPkg', name: '模板配置', component: './systemTools/autoPkg' }, + { path: '/systemTools/exportTemplate', name: '导出模板', component: './systemTools/exportTemplate' }, + { path: '/systemTools/picture', name: 'AI页面绘制', component: './systemTools/autoCode/picture' }, + { path: '/systemTools/mcpTool', name: 'Mcp Tools模板', component: './systemTools/autoCode/mcp' }, + { path: '/systemTools/mcpTest', name: 'Mcp Tools测试', component: './systemTools/autoCode/mcpTest' }, + { path: '/systemTools/sysVersion', name: '版本管理', component: './systemTools/version' }, + { path: '/systemTools/sysError', name: '错误日志', component: './systemTools/sysError' }, + { path: '/systemTools/installPlugin', name: '插件安装', component: './systemTools/installPlugin' }, + { path: '/systemTools/pubPlug', name: '打包插件', component: './systemTools/pubPlug' }, ], }, // 插件 { path: '/plugin', - name: 'plugin', - icon: 'appstore', + name: '插件系统', + hideInMenu: true, routes: [ - { - path: '/plugin/announcement', - name: 'announcement', - icon: 'notification', - component: './plugin/announcement', - }, - { - path: '/plugin/email', - name: 'email', - icon: 'mail', - component: './plugin/email', - }, + { path: '/plugin/announcement', name: '公告管理', component: './plugin/announcement' }, + { path: '/plugin/email', name: '邮件插件', component: './plugin/email' }, ], }, // 示例 { path: '/example', - name: 'example', - icon: 'experiment', + name: '示例文件', + hideInMenu: true, routes: [ - { - path: '/example/customer', - name: 'customer', - icon: 'contacts', - component: './example/customer', - }, - { - path: '/example/upload', - name: 'upload', - icon: 'upload', - component: './example/upload', - }, - { - path: '/example/upload/scan', - name: 'scanUpload', - icon: 'scan', - component: './example/upload/scanUpload', - hideInMenu: true, - }, - { - path: '/example/breakpoint', - name: 'breakpoint', - icon: 'cloud-upload', - component: './example/breakpoint', - }, + { path: '/example/customer', name: '客户列表', component: './example/customer' }, + { path: '/example/upload', name: '媒体库', component: './example/upload' }, + { path: '/example/breakpoint', name: '断点续传', component: './example/breakpoint' }, ], }, // 个人中心 { path: '/person', - name: 'person', - icon: 'user', + name: '个人信息', component: './person', hideInMenu: true, }, @@ -235,9 +130,17 @@ export default [ // 关于 { path: '/about', - name: 'about', - icon: 'info-circle', + name: '关于我们', component: './about', + hideInMenu: true, + }, + + // 服务器状态 + { + path: '/state', + name: '服务器状态', + component: './system/state', + hideInMenu: true, }, // 默认重定向 @@ -246,10 +149,9 @@ export default [ redirect: '/dashboard', }, - // 404 + // 404 - 放在最后 { - path: '*', - layout: false, + path: '/*', component: './error/404', }, ]; diff --git a/web/src/app.tsx b/web/src/app.tsx index 924375f..dfba454 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -1,11 +1,14 @@ /** * KRA - App Configuration * 应用运行时配置 + * 参考 GVA 的 permission.js 和 router store 实现动态路由 */ import { AvatarDropdown, AvatarName, Footer } from '@/components'; import CommandMenu from '@/components/CommandMenu'; import { getUserInfo } from '@/services/kratos/user'; -import { getToken, removeToken } from '@/utils/auth'; +import { asyncMenu } from '@/services/kratos/menu'; +import { getToken } from '@/utils/auth'; +import { asyncRouterHandle, BackendMenuItem } from '@/utils/dynamicRouter'; import { BulbOutlined, BulbFilled, @@ -15,11 +18,12 @@ import { QuestionCircleOutlined, SearchOutlined, } from '@ant-design/icons'; -import type { Settings as LayoutSettings } from '@ant-design/pro-components'; +import * as Icons from '@ant-design/icons'; +import type { Settings as LayoutSettings, MenuDataItem } from '@ant-design/pro-components'; import { SettingDrawer } from '@ant-design/pro-components'; import type { RequestConfig, RunTimeLayoutConfig } from '@umijs/max'; import { history } from '@umijs/max'; -import { message, Tooltip } from 'antd'; +import { Tooltip } from 'antd'; import React, { useState } from 'react'; import defaultSettings from '../config/defaultSettings'; import { errorConfig } from './requestErrorConfig'; @@ -27,7 +31,87 @@ import { errorConfig } from './requestErrorConfig'; const isDev = process.env.NODE_ENV === 'development'; const loginPath = '/user/login'; const initPath = '/init'; -const whiteList = [loginPath, initPath, '/error/403', '/error/404', '/error/500']; +const whiteList = [loginPath, initPath, '/error/403', '/error/404', '/error/500', '/scanUpload']; + +// 图标映射函数 +const iconMap: Record = {}; +Object.keys(Icons).forEach((key) => { + if (key.endsWith('Outlined') || key.endsWith('Filled') || key.endsWith('TwoTone')) { + const IconComponent = (Icons as any)[key]; + iconMap[key.toLowerCase().replace(/(outlined|filled|twotone)$/, '')] = ; + } +}); + +// 自定义图标映射(GVA 使用的图标名称) +const customIconMap: Record = { + 'odometer': , + 'user': , + 'avatar': , + 'tickets': , + 'platform': , + 'coordinate': , + 'notebook': , + 'pie-chart': , + 'compass': , + 'tools': , + 'cpu': , + 'magic-stick': , + 'operation': , + 'folder': , + 'reading': , + 'picture-filled': , + 'magnet': , + 'partly-cloudy': , + 'server': , + 'warn': , + 'cherry': , + 'shop': , + 'box': , + 'files': , + 'message': , + 'scaleToOriginal': , + 'upload': , + 'upload-filled': , + 'management': , + 'info-filled': , + 'cloudy': , + 'setting': , + 'customer-gva': , +}; + +function getIcon(iconName?: string): React.ReactNode { + if (!iconName) return undefined; + // 先查自定义映射 + if (customIconMap[iconName]) return customIconMap[iconName]; + // 再查 antd 图标 + const key = iconName.toLowerCase().replace(/-/g, ''); + if (iconMap[key]) return iconMap[key]; + return undefined; +} + +/** + * 将后端菜单数据转换为 ProLayout 菜单格式 + */ +function convertToMenuData(menus: BackendMenuItem[], parentPath: string = ''): MenuDataItem[] { + return menus.map((menu) => { + const path = menu.path.startsWith('/') || menu.path.startsWith('http') + ? menu.path + : `${parentPath}/${menu.path}`.replace(/\/+/g, '/'); + + const menuItem: MenuDataItem = { + path, + name: menu.meta?.title || menu.name, + icon: getIcon(menu.meta?.icon), + hideInMenu: menu.hidden, + }; + + if (menu.children && menu.children.length > 0) { + menuItem.children = convertToMenuData(menu.children, path); + } + + return menuItem; + }); +} /** * 获取初始状态 @@ -37,8 +121,12 @@ export async function getInitialState(): Promise<{ settings?: Partial; currentUser?: API.CurrentUser; loading?: boolean; + menus?: BackendMenuItem[]; + menuData?: MenuDataItem[]; fetchUserInfo?: () => Promise; + fetchMenus?: () => Promise; }> { + // 获取用户信息 const fetchUserInfo = async (): Promise => { try { const token = getToken(); @@ -46,7 +134,6 @@ export async function getInitialState(): Promise<{ return undefined; } const res = await getUserInfo(); - // request.ts 的 responseInterceptors 已经解包,直接使用 res if (res.code === 0 && res.data?.userInfo) { const userInfo = res.data.userInfo; return { @@ -58,10 +145,7 @@ export async function getInitialState(): Promise<{ headerImg: userInfo.headerImg, authority: userInfo.authority, authorities: userInfo.authorities, - // 按钮权限(如果后端返回) buttons: res.data.buttons || [], - // 菜单权限(如果后端返回) - menus: res.data.menus || [], }; } return undefined; @@ -71,41 +155,68 @@ export async function getInitialState(): Promise<{ } }; - // 如果是白名单页面,不获取用户信息 + // 获取动态菜单 - 对应 GVA 的 SetAsyncRouter + const fetchMenus = async (): Promise => { + try { + const token = getToken(); + if (!token) { + return []; + } + const res = await asyncMenu(); + if (res.code === 0 && res.data?.menus) { + return res.data.menus; + } + return []; + } catch (error) { + console.error('获取菜单失败:', error); + return []; + } + }; + + // 如果是白名单页面,不获取用户信息和菜单 const { location } = history; if (whiteList.some((path) => location.pathname.startsWith(path))) { return { fetchUserInfo, + fetchMenus, settings: defaultSettings as Partial, }; } - // 获取用户信息 + // 获取用户信息和菜单 const currentUser = await fetchUserInfo(); if (!currentUser) { - // 未登录,跳转到登录页 history.push(loginPath); + return { + fetchUserInfo, + fetchMenus, + settings: defaultSettings as Partial, + }; } + // 获取动态菜单 + const menus = await fetchMenus(); + const menuData = convertToMenuData(menus); + return { fetchUserInfo, + fetchMenus, currentUser, + menus, + menuData, settings: defaultSettings as Partial, }; } - /** * ProLayout 配置 * @see https://procomponents.ant.design/components/layout */ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => { - // 刷新页面 const handleRefresh = () => { window.location.reload(); }; - // 切换主题 const handleToggleTheme = () => { const newNavTheme = initialState?.settings?.navTheme === 'realDark' ? 'light' : 'realDark'; setInitialState((preInitialState) => ({ @@ -117,7 +228,6 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) = })); }; - // 打开设置 const handleOpenSetting = () => { const settingBtn = document.querySelector('.ant-pro-setting-drawer-handle'); if (settingBtn) { @@ -137,14 +247,20 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) = }; return { - // 先展开 settings,让后面的配置可以覆盖 ...initialState?.settings, - // 右上角操作区 + // 使用后端返回的菜单数据 + menu: { + locale: false, // 禁用国际化 + request: async () => { + // 返回已获取的菜单数据 + return initialState?.menuData || []; + }, + }, + actionsRender: () => { const isDarkTheme = initialState?.settings?.navTheme === 'realDark'; return [ - // 帮助文档 , - // 搜索 (Ctrl+K) window.dispatchEvent(new CustomEvent('openCommandMenu'))} @@ -164,19 +279,16 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) = , - // 系统设置 , - // 刷新 , - // 切换主题 {isDarkTheme ? ( @@ -189,7 +301,6 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) = ]; }, - // 头像配置 avatarProps: { src: initialState?.currentUser?.avatar || initialState?.currentUser?.headerImg || '/default-avatar.svg', title: , @@ -197,31 +308,46 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) = render: (_, avatarChildren) => {avatarChildren}, }, - // 水印 waterMarkProps: { content: initialState?.currentUser?.nickName || initialState?.currentUser?.userName, }, - // 页脚 footerRender: () =>