前端重构

This commit is contained in:
Yvan 2026-01-08 15:06:46 +08:00
parent b770c4a3b6
commit 010a96db19
10 changed files with 974 additions and 308 deletions

View File

@ -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',
},
];

View File

@ -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 (
<>

View File

@ -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>
);
};

View File

@ -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',
};

View File

@ -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': '退出登录',
};

View File

@ -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;

View File

@ -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;

View File

@ -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('已配置数据库信息,无法初始化');

View File

@ -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 {

View File

@ -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,
};