前端重构
This commit is contained in:
parent
b770c4a3b6
commit
010a96db19
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
187
web/src/app.tsx
187
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<string, React.ReactNode> = {};
|
||||
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)$/, '')] = <IconComponent />;
|
||||
}
|
||||
});
|
||||
|
||||
// 自定义图标映射(GVA 使用的图标名称)
|
||||
const customIconMap: Record<string, React.ReactNode> = {
|
||||
'odometer': <Icons.DashboardOutlined />,
|
||||
'user': <Icons.UserOutlined />,
|
||||
'avatar': <Icons.TeamOutlined />,
|
||||
'tickets': <Icons.MenuOutlined />,
|
||||
'platform': <Icons.ApiOutlined />,
|
||||
'coordinate': <Icons.UserOutlined />,
|
||||
'notebook': <Icons.BookOutlined />,
|
||||
'pie-chart': <Icons.PieChartOutlined />,
|
||||
'compass': <Icons.CompassOutlined />,
|
||||
'tools': <Icons.ToolOutlined />,
|
||||
'cpu': <Icons.CodeOutlined />,
|
||||
'magic-stick': <Icons.ThunderboltOutlined />,
|
||||
'operation': <Icons.SettingOutlined />,
|
||||
'folder': <Icons.FolderOutlined />,
|
||||
'reading': <Icons.FileTextOutlined />,
|
||||
'picture-filled': <Icons.PictureOutlined />,
|
||||
'magnet': <Icons.ApiOutlined />,
|
||||
'partly-cloudy': <Icons.CloudOutlined />,
|
||||
'server': <Icons.CloudServerOutlined />,
|
||||
'warn': <Icons.WarningOutlined />,
|
||||
'cherry': <Icons.AppstoreOutlined />,
|
||||
'shop': <Icons.ShopOutlined />,
|
||||
'box': <Icons.InboxOutlined />,
|
||||
'files': <Icons.FileOutlined />,
|
||||
'message': <Icons.MailOutlined />,
|
||||
'scaleToOriginal': <Icons.NotificationOutlined />,
|
||||
'upload': <Icons.UploadOutlined />,
|
||||
'upload-filled': <Icons.CloudUploadOutlined />,
|
||||
'management': <Icons.ExperimentOutlined />,
|
||||
'info-filled': <Icons.InfoCircleOutlined />,
|
||||
'cloudy': <Icons.CloudOutlined />,
|
||||
'setting': <Icons.SettingOutlined />,
|
||||
'customer-gva': <Icons.LinkOutlined />,
|
||||
};
|
||||
|
||||
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<LayoutSettings>;
|
||||
currentUser?: API.CurrentUser;
|
||||
loading?: boolean;
|
||||
menus?: BackendMenuItem[];
|
||||
menuData?: MenuDataItem[];
|
||||
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
|
||||
fetchMenus?: () => Promise<BackendMenuItem[]>;
|
||||
}> {
|
||||
// 获取用户信息
|
||||
const fetchUserInfo = async (): Promise<API.CurrentUser | undefined> => {
|
||||
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<BackendMenuItem[]> => {
|
||||
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<LayoutSettings>,
|
||||
};
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
// 获取用户信息和菜单
|
||||
const currentUser = await fetchUserInfo();
|
||||
if (!currentUser) {
|
||||
// 未登录,跳转到登录页
|
||||
history.push(loginPath);
|
||||
return {
|
||||
fetchUserInfo,
|
||||
fetchMenus,
|
||||
settings: defaultSettings as Partial<LayoutSettings>,
|
||||
};
|
||||
}
|
||||
|
||||
// 获取动态菜单
|
||||
const menus = await fetchMenus();
|
||||
const menuData = convertToMenuData(menus);
|
||||
|
||||
return {
|
||||
fetchUserInfo,
|
||||
fetchMenus,
|
||||
currentUser,
|
||||
menus,
|
||||
menuData,
|
||||
settings: defaultSettings as Partial<LayoutSettings>,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 [
|
||||
// 帮助文档
|
||||
<Tooltip title="帮助文档" key="doc">
|
||||
<a
|
||||
href="https://go-kratos.dev/"
|
||||
|
|
@ -155,7 +271,6 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
|
|||
<QuestionCircleOutlined style={{ fontSize: 16 }} />
|
||||
</a>
|
||||
</Tooltip>,
|
||||
// 搜索 (Ctrl+K)
|
||||
<Tooltip title="搜索 (Ctrl+K)" key="search">
|
||||
<span
|
||||
onClick={() => window.dispatchEvent(new CustomEvent('openCommandMenu'))}
|
||||
|
|
@ -164,19 +279,16 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
|
|||
<SearchOutlined style={{ fontSize: 16 }} />
|
||||
</span>
|
||||
</Tooltip>,
|
||||
// 系统设置
|
||||
<Tooltip title="系统设置" key="setting">
|
||||
<span onClick={handleOpenSetting} style={iconStyle}>
|
||||
<SettingOutlined style={{ fontSize: 16 }} />
|
||||
</span>
|
||||
</Tooltip>,
|
||||
// 刷新
|
||||
<Tooltip title="刷新" key="refresh">
|
||||
<span onClick={handleRefresh} style={iconStyle}>
|
||||
<ReloadOutlined style={{ fontSize: 16 }} />
|
||||
</span>
|
||||
</Tooltip>,
|
||||
// 切换主题
|
||||
<Tooltip title="切换主题" key="theme">
|
||||
<span onClick={handleToggleTheme} style={iconStyle}>
|
||||
{isDarkTheme ? (
|
||||
|
|
@ -189,7 +301,6 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
|
|||
];
|
||||
},
|
||||
|
||||
// 头像配置
|
||||
avatarProps: {
|
||||
src: initialState?.currentUser?.avatar || initialState?.currentUser?.headerImg || '/default-avatar.svg',
|
||||
title: <AvatarName />,
|
||||
|
|
@ -197,31 +308,46 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
|
|||
render: (_, avatarChildren) => <AvatarDropdown menu>{avatarChildren}</AvatarDropdown>,
|
||||
},
|
||||
|
||||
// 水印
|
||||
waterMarkProps: {
|
||||
content: initialState?.currentUser?.nickName || initialState?.currentUser?.userName,
|
||||
},
|
||||
|
||||
// 页脚
|
||||
footerRender: () => <Footer />,
|
||||
|
||||
// 页面切换时的回调
|
||||
onPageChange: () => {
|
||||
const { location } = history;
|
||||
// 白名单页面不检查登录状态
|
||||
if (whiteList.some((path) => location.pathname.startsWith(path))) {
|
||||
return;
|
||||
}
|
||||
// 如果没有登录,重定向到登录页
|
||||
if (!initialState?.currentUser) {
|
||||
history.push(loginPath);
|
||||
}
|
||||
},
|
||||
|
||||
// 菜单头部渲染
|
||||
menuHeaderRender: undefined,
|
||||
|
||||
// 自定义 403 页面
|
||||
// 菜单项渲染 - 处理外部链接
|
||||
menuItemRender: (item, dom) => {
|
||||
if (item.path?.startsWith('http')) {
|
||||
return (
|
||||
<a href={item.path} target="_blank" rel="noopener noreferrer">
|
||||
{dom}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a
|
||||
onClick={() => {
|
||||
if (item.path) {
|
||||
history.push(item.path);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{dom}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
|
||||
unAccessible: (
|
||||
<div style={{ textAlign: 'center', padding: '100px 0' }}>
|
||||
<h1>403</h1>
|
||||
|
|
@ -229,7 +355,6 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
|
|||
</div>
|
||||
),
|
||||
|
||||
// 子元素渲染
|
||||
childrenRender: (children) => {
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
|
||||
interface BottomInfoProps {
|
||||
export interface BottomInfoProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const BottomInfo: React.FC<BottomInfoProps> = ({ className, style }) => {
|
||||
const BottomInfo: React.FC<BottomInfoProps> = ({ className, style, children }) => {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
|
|
@ -25,21 +26,25 @@ const BottomInfo: React.FC<BottomInfoProps> = ({ className, style }) => {
|
|||
...style,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span style={{ marginRight: 4 }}>Powered by</span>
|
||||
<a
|
||||
href="https://github.com/go-kratos/kratos"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ fontWeight: 600, color: '#1890ff' }}
|
||||
>
|
||||
Kratos Admin
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ marginRight: 4 }}>Copyright © {new Date().getFullYear()}</span>
|
||||
<span>KRA Team</span>
|
||||
</div>
|
||||
{children || (
|
||||
<>
|
||||
<div>
|
||||
<span style={{ marginRight: 4 }}>Powered by</span>
|
||||
<a
|
||||
href="https://github.com/go-kratos/kratos"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ fontWeight: 600, color: '#1890ff' }}
|
||||
>
|
||||
Kratos Admin
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ marginRight: 4 }}>Copyright © {new Date().getFullYear()}</span>
|
||||
<span>KRA Team</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,52 +1,66 @@
|
|||
export default {
|
||||
// Basic menu
|
||||
'menu.welcome': 'Welcome',
|
||||
'menu.more-blocks': 'More Blocks',
|
||||
'menu.home': 'Home',
|
||||
'menu.admin': 'Admin',
|
||||
'menu.admin.sub-page': 'Sub-Page',
|
||||
'menu.login': 'Login',
|
||||
'menu.register': 'Register',
|
||||
'menu.register-result': 'Register Result',
|
||||
|
||||
// Dashboard
|
||||
'menu.dashboard': 'Dashboard',
|
||||
'menu.dashboard.analysis': 'Analysis',
|
||||
'menu.dashboard.monitor': 'Monitor',
|
||||
'menu.dashboard.workplace': 'Workplace',
|
||||
|
||||
// Super Admin
|
||||
'menu.superAdmin': 'Super Admin',
|
||||
'menu.superAdmin.user': 'User Management',
|
||||
'menu.superAdmin.authority': 'Role Management',
|
||||
'menu.superAdmin.menu': 'Menu Management',
|
||||
'menu.superAdmin.api': 'API Management',
|
||||
'menu.superAdmin.operation': 'Operation History',
|
||||
'menu.superAdmin.dictionary': 'Dictionary Management',
|
||||
'menu.superAdmin.dictionaryDetail': 'Dictionary Detail',
|
||||
'menu.superAdmin.params': 'Params Management',
|
||||
|
||||
// System Tools
|
||||
'menu.systemTools': 'System Tools',
|
||||
'menu.systemTools.system': 'System Config',
|
||||
'menu.systemTools.version': 'Version Management',
|
||||
'menu.systemTools.sysError': 'Error Logs',
|
||||
'menu.systemTools.autoCode': 'Code Generator',
|
||||
'menu.systemTools.autoCodeAdmin': 'Auto Code Admin',
|
||||
'menu.systemTools.exportTemplate': 'Export Template',
|
||||
'menu.systemTools.formCreate': 'Form Generator',
|
||||
'menu.systemTools.installPlugin': 'Install Plugin',
|
||||
'menu.systemTools.pubPlug': 'Publish Plugin',
|
||||
|
||||
// Plugin System
|
||||
'menu.plugin': 'Plugin System',
|
||||
'menu.plugin.announcement': 'Announcement',
|
||||
'menu.plugin.email': 'Email Plugin',
|
||||
|
||||
// Example
|
||||
'menu.example': 'Example',
|
||||
'menu.example.customer': 'Customer List',
|
||||
'menu.example.upload': 'Media Library',
|
||||
'menu.example.scanUpload': 'Scan Upload',
|
||||
'menu.example.breakpoint': 'Breakpoint Upload',
|
||||
|
||||
// Person
|
||||
'menu.person': 'Personal Info',
|
||||
|
||||
// About
|
||||
'menu.about': 'About Us',
|
||||
|
||||
// Exception
|
||||
'menu.exception': 'Exception',
|
||||
'menu.exception.403': '403',
|
||||
'menu.exception.404': '404',
|
||||
'menu.exception.500': '500',
|
||||
'menu.form': 'Form',
|
||||
'menu.form.basic-form': 'Basic Form',
|
||||
'menu.form.step-form': 'Step Form',
|
||||
'menu.form.step-form.info': 'Step Form(write transfer information)',
|
||||
'menu.form.step-form.confirm': 'Step Form(confirm transfer information)',
|
||||
'menu.form.step-form.result': 'Step Form(finished)',
|
||||
'menu.form.advanced-form': 'Advanced Form',
|
||||
'menu.list': 'List',
|
||||
'menu.list.table-list': 'Search Table',
|
||||
'menu.list.basic-list': 'Basic List',
|
||||
'menu.list.card-list': 'Card List',
|
||||
'menu.list.search-list': 'Search List',
|
||||
'menu.list.search-list.articles': 'Search List(articles)',
|
||||
'menu.list.search-list.projects': 'Search List(projects)',
|
||||
'menu.list.search-list.applications': 'Search List(applications)',
|
||||
'menu.profile': 'Profile',
|
||||
'menu.profile.basic': 'Basic Profile',
|
||||
'menu.profile.advanced': 'Advanced Profile',
|
||||
'menu.result': 'Result',
|
||||
'menu.result.success': 'Success',
|
||||
'menu.result.fail': 'Fail',
|
||||
'menu.exception': 'Exception',
|
||||
'menu.exception.not-permission': '403',
|
||||
'menu.exception.not-find': '404',
|
||||
'menu.exception.server-error': '500',
|
||||
'menu.exception.trigger': 'Trigger',
|
||||
|
||||
// Account
|
||||
'menu.account': 'Account',
|
||||
'menu.account.center': 'Account Center',
|
||||
'menu.account.settings': 'Account Settings',
|
||||
'menu.account.trigger': 'Trigger Error',
|
||||
'menu.account.logout': 'Logout',
|
||||
'menu.editor': 'Graphic Editor',
|
||||
'menu.editor.flow': 'Flow Editor',
|
||||
'menu.editor.mind': 'Mind Editor',
|
||||
'menu.editor.koni': 'Koni Editor',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,51 +1,66 @@
|
|||
export default {
|
||||
"menu.welcome": "欢迎",
|
||||
"menu.more-blocks": "更多区块",
|
||||
"menu.home": "首页",
|
||||
"menu.admin": "管理员",
|
||||
"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.home': '首页',
|
||||
'menu.login': '登录',
|
||||
'menu.register': '注册',
|
||||
|
||||
// 仪表盘
|
||||
'menu.dashboard': '仪表盘',
|
||||
|
||||
// 超级管理员
|
||||
'menu.superAdmin': '超级管理员',
|
||||
'menu.superAdmin.user': '用户管理',
|
||||
'menu.superAdmin.authority': '角色管理',
|
||||
'menu.superAdmin.menu': '菜单管理',
|
||||
'menu.superAdmin.api': 'API管理',
|
||||
'menu.superAdmin.operation': '操作历史',
|
||||
'menu.superAdmin.dictionary': '字典管理',
|
||||
'menu.superAdmin.dictionaryDetail': '字典详情',
|
||||
'menu.superAdmin.params': '参数管理',
|
||||
|
||||
// 系统工具
|
||||
'menu.systemTools': '系统工具',
|
||||
'menu.systemTools.system': '系统配置',
|
||||
'menu.systemTools.version': '版本管理',
|
||||
'menu.systemTools.sysError': '错误日志',
|
||||
'menu.systemTools.autoCode': '代码生成器',
|
||||
'menu.systemTools.autoCodeAdmin': '自动化代码管理',
|
||||
'menu.systemTools.exportTemplate': '导出模板',
|
||||
'menu.systemTools.formCreate': '表单生成器',
|
||||
'menu.systemTools.installPlugin': '插件安装',
|
||||
'menu.systemTools.pubPlug': '打包插件',
|
||||
|
||||
// 插件系统
|
||||
'menu.plugin': '插件系统',
|
||||
'menu.plugin.announcement': '公告管理',
|
||||
'menu.plugin.email': '邮件插件',
|
||||
|
||||
// 示例文件
|
||||
'menu.example': '示例文件',
|
||||
'menu.example.customer': '客户列表',
|
||||
'menu.example.upload': '媒体库',
|
||||
'menu.example.scanUpload': '扫码上传',
|
||||
'menu.example.breakpoint': '断点续传',
|
||||
|
||||
// 个人中心
|
||||
'menu.person': '个人信息',
|
||||
|
||||
// 关于
|
||||
'menu.about': '关于我们',
|
||||
|
||||
// 异常页面
|
||||
'menu.exception': '异常页',
|
||||
'menu.exception.403': '403',
|
||||
'menu.exception.404': '404',
|
||||
'menu.exception.500': '500',
|
||||
'menu.exception.not-permission': '403',
|
||||
'menu.exception.not-find': '404',
|
||||
'menu.exception.server-error': '500',
|
||||
|
||||
// 账户相关
|
||||
'menu.account': '个人页',
|
||||
'menu.account.center': '个人中心',
|
||||
'menu.account.settings': '个人设置',
|
||||
'menu.account.logout': '退出登录',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
/**
|
||||
* KRA - 服务器状态页面
|
||||
* 对应 GVA: view/systemTools/system/system.vue 中的服务器状态部分
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Card, Row, Col, Progress, Statistic, Spin } from 'antd';
|
||||
import { getServerInfo } from '@/services/kratos/system';
|
||||
|
||||
interface ServerInfo {
|
||||
cpu: {
|
||||
cpuNum: number;
|
||||
cpuPercent: number[];
|
||||
};
|
||||
mem: {
|
||||
total: number;
|
||||
used: number;
|
||||
usedPercent: number;
|
||||
};
|
||||
disk: {
|
||||
total: number;
|
||||
used: number;
|
||||
usedPercent: number;
|
||||
};
|
||||
os: {
|
||||
goVersion: string;
|
||||
os: string;
|
||||
arch: string;
|
||||
numCpu: number;
|
||||
compiler: string;
|
||||
version: string;
|
||||
numGoroutine: number;
|
||||
};
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const State: React.FC = () => {
|
||||
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchServerInfo = async () => {
|
||||
try {
|
||||
const res = await getServerInfo();
|
||||
if (res.code === 0) {
|
||||
setServerInfo(res.data);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchServerInfo();
|
||||
const timer = setInterval(fetchServerInfo, 5000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<div style={{ textAlign: 'center', padding: 100 }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* CPU 使用率 */}
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card title="CPU 使用率">
|
||||
<Progress
|
||||
type="dashboard"
|
||||
percent={serverInfo?.cpu?.cpuPercent?.[0] || 0}
|
||||
format={(percent) => `${percent?.toFixed(1)}%`}
|
||||
/>
|
||||
<Statistic title="CPU 核心数" value={serverInfo?.cpu?.cpuNum || 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 内存使用 */}
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card title="内存使用">
|
||||
<Progress
|
||||
type="dashboard"
|
||||
percent={serverInfo?.mem?.usedPercent || 0}
|
||||
format={(percent) => `${percent?.toFixed(1)}%`}
|
||||
/>
|
||||
<Statistic
|
||||
title="已用/总量"
|
||||
value={`${formatBytes(serverInfo?.mem?.used || 0)} / ${formatBytes(serverInfo?.mem?.total || 0)}`}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 磁盘使用 */}
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card title="磁盘使用">
|
||||
<Progress
|
||||
type="dashboard"
|
||||
percent={serverInfo?.disk?.usedPercent || 0}
|
||||
format={(percent) => `${percent?.toFixed(1)}%`}
|
||||
/>
|
||||
<Statistic
|
||||
title="已用/总量"
|
||||
value={`${formatBytes(serverInfo?.disk?.used || 0)} / ${formatBytes(serverInfo?.disk?.total || 0)}`}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 系统信息 */}
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card title="系统信息">
|
||||
<p><strong>Go 版本:</strong> {serverInfo?.os?.goVersion}</p>
|
||||
<p><strong>系统:</strong> {serverInfo?.os?.os}</p>
|
||||
<p><strong>架构:</strong> {serverInfo?.os?.arch}</p>
|
||||
<p><strong>Goroutine:</strong> {serverInfo?.os?.numGoroutine}</p>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default State;
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
/**
|
||||
* KRA - 模板配置页面
|
||||
* 对应 GVA: view/systemTools/autoPkg/autoPkg.vue
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Table, Button, Drawer, Form, Input, Select, Space, message, Modal } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import WarningBar from '@/components/WarningBar/WarningBar';
|
||||
import { createPackage, getPackage, deletePackage, getTemplates } from '@/services/kratos/autoCode';
|
||||
|
||||
interface PackageItem {
|
||||
ID: number;
|
||||
packageName: string;
|
||||
template: string;
|
||||
label: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
const AutoPkg: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [tableData, setTableData] = useState<PackageItem[]>([]);
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [templatesOptions, setTemplatesOptions] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 获取模板列表
|
||||
const fetchTemplates = async () => {
|
||||
const res = await getTemplates();
|
||||
if (res.code === 0) {
|
||||
setTemplatesOptions(res.data || []);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取表格数据
|
||||
const fetchTableData = async () => {
|
||||
setLoading(true);
|
||||
const res = await getPackage();
|
||||
if (res.code === 0) {
|
||||
setTableData(res.data?.pkgs || []);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTemplates();
|
||||
fetchTableData();
|
||||
}, []);
|
||||
|
||||
// 打开抽屉
|
||||
const openDrawer = () => {
|
||||
setDrawerVisible(true);
|
||||
};
|
||||
|
||||
// 关闭抽屉
|
||||
const closeDrawer = () => {
|
||||
setDrawerVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const res = await createPackage(values);
|
||||
if (res.code === 0) {
|
||||
message.success('添加成功');
|
||||
fetchTableData();
|
||||
closeDrawer();
|
||||
}
|
||||
} catch (error) {
|
||||
// 表单验证失败
|
||||
}
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = (record: PackageItem) => {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '此操作仅删除数据库中的pkg存储,后端相应目录结构请自行删除与数据库保持一致!',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const res = await deletePackage({ packageName: record.packageName });
|
||||
if (res.code === 0) {
|
||||
message.success('删除成功');
|
||||
fetchTableData();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 自定义验证器
|
||||
const validatePackageName = (_: any, value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject('请输入包名');
|
||||
}
|
||||
if (/[\u4E00-\u9FA5]/g.test(value)) {
|
||||
return Promise.reject('不能为中文');
|
||||
}
|
||||
if (/^\d+$/.test(value[0])) {
|
||||
return Promise.reject('不能够以数字开头');
|
||||
}
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(value)) {
|
||||
return Promise.reject('只能包含英文字母、数字和下划线');
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'ID', key: 'ID', width: 120 },
|
||||
{ title: '包名', dataIndex: 'packageName', key: 'packageName', width: 150 },
|
||||
{ title: '模板', dataIndex: 'template', key: 'template', width: 150 },
|
||||
{ title: '展示名', dataIndex: 'label', key: 'label', width: 150 },
|
||||
{ title: '描述', dataIndex: 'desc', key: 'desc' },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
render: (_: any, record: PackageItem) => (
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<WarningBar
|
||||
title="模板配置"
|
||||
href="https://www.gin-vue-admin.com/docs/auto_code"
|
||||
/>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openDrawer}>
|
||||
新增
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey="ID"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
/>
|
||||
<Drawer
|
||||
title="新增模板配置"
|
||||
width={500}
|
||||
open={drawerVisible}
|
||||
onClose={closeDrawer}
|
||||
extra={
|
||||
<Space>
|
||||
<Button onClick={closeDrawer}>取消</Button>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
确定
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="包名"
|
||||
name="packageName"
|
||||
rules={[{ validator: validatePackageName }]}
|
||||
>
|
||||
<Input placeholder="请输入包名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="模板"
|
||||
name="template"
|
||||
rules={[{ required: true, message: '请选择模板' }]}
|
||||
>
|
||||
<Select placeholder="请选择模板">
|
||||
{templatesOptions.map((item) => (
|
||||
<Select.Option key={item} value={item}>
|
||||
{item}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="展示名"
|
||||
name="label"
|
||||
rules={[{ required: true, message: '请输入展示名' }]}
|
||||
>
|
||||
<Input placeholder="请输入展示名" />
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="desc">
|
||||
<Input.TextArea placeholder="请输入描述" rows={3} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoPkg;
|
||||
|
|
@ -10,6 +10,7 @@ import { flushSync } from 'react-dom';
|
|||
import { login, captcha } from '@/services/kratos/user';
|
||||
import { checkDB } from '@/services/kratos/initdb';
|
||||
import { useUserStore } from '@/models/user';
|
||||
import { convertToMenuData } from '@/utils/dynamicRouter';
|
||||
import Logo from '@/components/Logo/Logo';
|
||||
import BottomInfo from '@/components/BottomInfo/BottomInfo';
|
||||
import { createStyles } from 'antd-style';
|
||||
|
|
@ -181,7 +182,7 @@ const Login: React.FC = () => {
|
|||
fetchCaptcha();
|
||||
}, [fetchCaptcha]);
|
||||
|
||||
// 提交登录
|
||||
// 提交登录 - 参考 GVA 的 LoginIn 逻辑
|
||||
const handleSubmit = async (values: LoginFormData) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
|
|
@ -192,7 +193,6 @@ const Login: React.FC = () => {
|
|||
captchaId: captchaData.captchaId,
|
||||
});
|
||||
|
||||
// request.ts 的 responseInterceptors 已经解包,直接使用 res
|
||||
if (res.code === 0 && res.data) {
|
||||
const { token, user } = res.data;
|
||||
|
||||
|
|
@ -200,21 +200,37 @@ const Login: React.FC = () => {
|
|||
setToken(token);
|
||||
setUserInfo(user);
|
||||
|
||||
// 更新全局状态,重新获取用户信息(包含菜单权限)
|
||||
const userInfo = await initialState?.fetchUserInfo?.();
|
||||
// 更新全局状态,重新获取用户信息和菜单
|
||||
const [userInfo, menus] = await Promise.all([
|
||||
initialState?.fetchUserInfo?.(),
|
||||
initialState?.fetchMenus?.(),
|
||||
]);
|
||||
|
||||
// 转换菜单数据
|
||||
const menuData = menus ? convertToMenuData(menus) : [];
|
||||
|
||||
flushSync(() => {
|
||||
setInitialState((state) => ({
|
||||
...state,
|
||||
currentUser: userInfo,
|
||||
menus,
|
||||
menuData,
|
||||
}));
|
||||
});
|
||||
|
||||
messageApi.success('登录成功!');
|
||||
|
||||
// 跳转到首页或重定向页面
|
||||
// 跳转到用户默认首页或重定向页面
|
||||
const urlParams = new URL(window.location.href).searchParams;
|
||||
const redirect = urlParams.get('redirect') || '/';
|
||||
history.push(redirect);
|
||||
const redirect = urlParams.get('redirect');
|
||||
|
||||
if (redirect) {
|
||||
history.push(redirect);
|
||||
} else {
|
||||
// 使用用户角色的默认路由
|
||||
const defaultRouter = user?.authority?.defaultRouter || 'dashboard';
|
||||
history.push(`/${defaultRouter}`);
|
||||
}
|
||||
} else {
|
||||
messageApi.error(res.msg || '登录失败,请重试');
|
||||
fetchCaptcha();
|
||||
|
|
@ -232,8 +248,9 @@ const Login: React.FC = () => {
|
|||
try {
|
||||
const res = await checkDB();
|
||||
// request.ts 的 responseInterceptors 已经解包,直接使用 res
|
||||
if (res.code === 0) {
|
||||
if (res.data?.needInit) {
|
||||
if (res.code === 0 && res.data) {
|
||||
const checkResult = res.data as { needInit?: boolean };
|
||||
if (checkResult.needInit) {
|
||||
history.push('/init');
|
||||
} else {
|
||||
messageApi.info('已配置数据库信息,无法初始化');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
* KRA - Database Init API Service
|
||||
*/
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResponse } from '@/types/api';
|
||||
|
||||
export interface InitDBParams {
|
||||
dbType: string;
|
||||
|
|
@ -20,12 +19,12 @@ export interface CheckDBResult {
|
|||
|
||||
/** 初始化数据库 */
|
||||
export const initDB = (data: InitDBParams) => {
|
||||
return request.post<ApiResponse<any>>('/init/initdb', data);
|
||||
return request.post<any>('/init/initdb', data);
|
||||
};
|
||||
|
||||
/** 检查数据库是否需要初始化 */
|
||||
export const checkDB = () => {
|
||||
return request.post<ApiResponse<CheckDBResult>>('/init/checkdb');
|
||||
return request.post<CheckDBResult>('/init/checkdb');
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,252 @@
|
|||
/**
|
||||
* KRA - Dynamic Router Utility
|
||||
* 动态路由处理工具,参考 GVA 的 asyncRouter.js 实现
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
// 页面组件映射 - 对应 GVA 的 viewModules
|
||||
// UMI 使用 React.lazy 动态导入
|
||||
const pageModules: Record<string, () => Promise<{ default: React.ComponentType<any> }>> = {
|
||||
// 仪表盘
|
||||
'view/dashboard/index.vue': () => import('@/pages/dashboard'),
|
||||
|
||||
// 超级管理员
|
||||
'view/superAdmin/authority/authority.vue': () => import('@/pages/system/authority'),
|
||||
'view/superAdmin/menu/menu.vue': () => import('@/pages/system/menu'),
|
||||
'view/superAdmin/api/api.vue': () => import('@/pages/system/api'),
|
||||
'view/superAdmin/user/user.vue': () => import('@/pages/system/user'),
|
||||
'view/superAdmin/dictionary/sysDictionary.vue': () => import('@/pages/system/dictionary'),
|
||||
'view/superAdmin/operation/sysOperationRecord.vue': () => import('@/pages/system/operation'),
|
||||
'view/superAdmin/params/sysParams.vue': () => import('@/pages/system/params'),
|
||||
|
||||
// 系统工具
|
||||
'view/systemTools/autoCode/index.vue': () => import('@/pages/systemTools/autoCode'),
|
||||
'view/systemTools/formCreate/index.vue': () => import('@/pages/systemTools/formCreate'),
|
||||
'view/systemTools/system/system.vue': () => import('@/pages/systemTools/system'),
|
||||
'view/systemTools/autoCodeAdmin/index.vue': () => import('@/pages/systemTools/autoCodeAdmin'),
|
||||
'view/systemTools/autoPkg/autoPkg.vue': () => import('@/pages/systemTools/autoPkg'),
|
||||
'view/systemTools/exportTemplate/exportTemplate.vue': () => import('@/pages/systemTools/exportTemplate'),
|
||||
'view/systemTools/installPlugin/index.vue': () => import('@/pages/systemTools/installPlugin'),
|
||||
'view/systemTools/pubPlug/pubPlug.vue': () => import('@/pages/systemTools/pubPlug'),
|
||||
'view/systemTools/version/version.vue': () => import('@/pages/systemTools/version'),
|
||||
'view/systemTools/sysError/sysError.vue': () => import('@/pages/systemTools/sysError'),
|
||||
|
||||
// 示例
|
||||
'view/example/upload/upload.vue': () => import('@/pages/example/upload'),
|
||||
'view/example/breakpoint/breakpoint.vue': () => import('@/pages/example/breakpoint'),
|
||||
'view/example/customer/customer.vue': () => import('@/pages/example/customer'),
|
||||
|
||||
// 插件
|
||||
'plugin/announcement/view/info.vue': () => import('@/pages/plugin/announcement'),
|
||||
'plugin/email/view/index.vue': () => import('@/pages/plugin/email'),
|
||||
|
||||
// 个人中心
|
||||
'view/person/person.vue': () => import('@/pages/person'),
|
||||
|
||||
// 关于
|
||||
'view/about/index.vue': () => import('@/pages/about'),
|
||||
|
||||
// 服务器状态
|
||||
'view/system/state.vue': () => import('@/pages/system/state'),
|
||||
|
||||
// 错误页面
|
||||
'view/error/reload.vue': () => import('@/pages/error/reload'),
|
||||
};
|
||||
|
||||
// 插件组件映射
|
||||
const pluginModules: Record<string, () => Promise<{ default: React.ComponentType<any> }>> = {
|
||||
'plugin/announcement/view/info.vue': () => import('@/pages/plugin/announcement'),
|
||||
'plugin/email/view/index.vue': () => import('@/pages/plugin/email'),
|
||||
};
|
||||
|
||||
export interface BackendMenuItem {
|
||||
ID?: number;
|
||||
parentId?: number;
|
||||
path: string;
|
||||
name: string;
|
||||
hidden?: boolean;
|
||||
component?: string;
|
||||
sort?: number;
|
||||
meta?: {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
keepAlive?: boolean;
|
||||
defaultMenu?: boolean;
|
||||
closeTab?: boolean;
|
||||
};
|
||||
children?: BackendMenuItem[];
|
||||
btns?: Record<string, number>;
|
||||
parameters?: Array<{ key: string; value: string }>;
|
||||
}
|
||||
|
||||
export interface RouteItem {
|
||||
path: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
component?: React.ComponentType<any> | React.LazyExoticComponent<any>;
|
||||
routes?: RouteItem[];
|
||||
hideInMenu?: boolean;
|
||||
// UMI Pro Layout 需要的字段
|
||||
access?: string;
|
||||
// 自定义 meta 信息
|
||||
meta?: {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
keepAlive?: boolean;
|
||||
defaultMenu?: boolean;
|
||||
closeTab?: boolean;
|
||||
btns?: Record<string, number>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态导入组件
|
||||
* 对应 GVA 的 dynamicImport 函数
|
||||
*/
|
||||
function dynamicImport(component: string): React.LazyExoticComponent<any> | null {
|
||||
// 先检查页面模块
|
||||
if (pageModules[component]) {
|
||||
return React.lazy(pageModules[component]);
|
||||
}
|
||||
|
||||
// 再检查插件模块
|
||||
if (component.startsWith('plugin/') && pluginModules[component]) {
|
||||
return React.lazy(pluginModules[component]);
|
||||
}
|
||||
|
||||
console.warn(`Component not found: ${component}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为外部链接
|
||||
*/
|
||||
function isExternalUrl(val: string | undefined): boolean {
|
||||
return typeof val === 'string' && /^(https?:)?\/\//.test(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理后端返回的菜单数据,转换为 UMI 路由格式
|
||||
* 对应 GVA 的 asyncRouterHandle 函数
|
||||
*/
|
||||
export function asyncRouterHandle(asyncRouter: BackendMenuItem[]): RouteItem[] {
|
||||
const routes: RouteItem[] = [];
|
||||
|
||||
asyncRouter.forEach((item) => {
|
||||
// 跳过外部链接
|
||||
if (isExternalUrl(item.path) || isExternalUrl(item.name) || isExternalUrl(item.component)) {
|
||||
// 外部链接作为菜单项但不作为路由
|
||||
routes.push({
|
||||
path: item.path,
|
||||
name: item.meta?.title || item.name,
|
||||
icon: item.meta?.icon,
|
||||
hideInMenu: item.hidden,
|
||||
meta: {
|
||||
title: item.meta?.title,
|
||||
icon: item.meta?.icon,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const route: RouteItem = {
|
||||
path: item.path.startsWith('/') ? item.path : `/${item.path}`,
|
||||
name: item.meta?.title || item.name,
|
||||
icon: item.meta?.icon,
|
||||
hideInMenu: item.hidden,
|
||||
meta: {
|
||||
title: item.meta?.title,
|
||||
icon: item.meta?.icon,
|
||||
keepAlive: item.meta?.keepAlive,
|
||||
defaultMenu: item.meta?.defaultMenu,
|
||||
closeTab: item.meta?.closeTab,
|
||||
btns: item.btns,
|
||||
},
|
||||
};
|
||||
|
||||
// 处理组件
|
||||
if (item.component && typeof item.component === 'string') {
|
||||
const component = dynamicImport(item.component);
|
||||
if (component) {
|
||||
route.component = component;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理子路由
|
||||
if (item.children && item.children.length > 0) {
|
||||
route.routes = asyncRouterHandle(item.children);
|
||||
}
|
||||
|
||||
routes.push(route);
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扁平化路由(用于路由注册)
|
||||
*/
|
||||
export function flattenRoutes(routes: RouteItem[], parentPath: string = ''): RouteItem[] {
|
||||
const result: RouteItem[] = [];
|
||||
|
||||
routes.forEach((route) => {
|
||||
const fullPath = route.path.startsWith('/')
|
||||
? route.path
|
||||
: `${parentPath}/${route.path}`.replace(/\/+/g, '/');
|
||||
|
||||
result.push({
|
||||
...route,
|
||||
path: fullPath,
|
||||
routes: undefined,
|
||||
});
|
||||
|
||||
if (route.routes && route.routes.length > 0) {
|
||||
result.push(...flattenRoutes(route.routes, fullPath));
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建菜单数据(用于侧边栏显示)
|
||||
*/
|
||||
export function buildMenuData(routes: RouteItem[]): RouteItem[] {
|
||||
return routes.filter((route) => !route.hideInMenu).map((route) => ({
|
||||
...route,
|
||||
routes: route.routes ? buildMenuData(route.routes) : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将后端菜单数据转换为 ProLayout 菜单格式
|
||||
* 与 app.tsx 中的函数保持一致
|
||||
*/
|
||||
export function convertToMenuData(menus: BackendMenuItem[], parentPath: string = ''): any[] {
|
||||
return menus.map((menu) => {
|
||||
const path = menu.path.startsWith('/') || menu.path.startsWith('http')
|
||||
? menu.path
|
||||
: `${parentPath}/${menu.path}`.replace(/\/+/g, '/');
|
||||
|
||||
const menuItem: any = {
|
||||
path,
|
||||
name: menu.meta?.title || menu.name,
|
||||
icon: menu.meta?.icon,
|
||||
hideInMenu: menu.hidden,
|
||||
};
|
||||
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menuItem.children = convertToMenuData(menu.children, path);
|
||||
}
|
||||
|
||||
return menuItem;
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
asyncRouterHandle,
|
||||
flattenRoutes,
|
||||
buildMenuData,
|
||||
isExternalUrl,
|
||||
convertToMenuData,
|
||||
};
|
||||
Loading…
Reference in New Issue