diff --git a/web/config/config.ts b/web/config/config.ts index 1a98ebe..d0d731b 100644 --- a/web/config/config.ts +++ b/web/config/config.ts @@ -168,7 +168,7 @@ export default defineConfig({ mock: { include: ["mock/**/*", "src/pages/**/_mock.ts"], }, - utoopack: {}, + // utoopack: {}, requestRecord: {}, exportStatic: {}, define: { diff --git a/web/config/defaultSettings.ts b/web/config/defaultSettings.ts index 6710cad..fed2469 100644 --- a/web/config/defaultSettings.ts +++ b/web/config/defaultSettings.ts @@ -1,28 +1,50 @@ import type { ProLayoutProps } from '@ant-design/pro-components'; /** - * @name + * KRA - Default Settings + * 默认布局配置 */ const Settings: ProLayoutProps & { pwa?: boolean; logo?: string; } = { navTheme: 'light', - // 拂晓蓝 + // 主题色 - 拂晓蓝 colorPrimary: '#1890ff', + // 布局模式: side | top | mix layout: 'mix', + // 内容宽度: Fluid | Fixed contentWidth: 'Fluid', - fixedHeader: false, + // 固定头部 + fixedHeader: true, + // 固定侧边栏 fixSiderbar: true, + // 色弱模式 colorWeak: false, - title: 'Ant Design Pro', - pwa: true, - logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', + // 标题 + title: 'Kratos Admin', + // PWA + pwa: false, + // Logo + logo: '/logo.svg', + // 图标字体 iconfontUrl: '', + // Token 配置 token: { - // 参见ts声明,demo 见文档,通过token 修改样式 - //https://procomponents.ant.design/components/layout#%E9%80%9A%E8%BF%87-token-%E4%BF%AE%E6%94%B9%E6%A0%B7%E5%BC%8F + // 侧边栏背景色 + sider: { + colorMenuBackground: '#fff', + colorTextMenu: 'rgba(0, 0, 0, 0.65)', + colorTextMenuSelected: '#1890ff', + colorBgMenuItemSelected: '#e6f7ff', + }, }, + // 分割菜单 + splitMenus: false, + // 页脚渲染 + footerRender: true, + // 菜单头部渲染 + menuHeaderRender: true, }; export default Settings; diff --git a/web/config/routes.ts b/web/config/routes.ts index c8fbf91..83f5e17 100644 --- a/web/config/routes.ts +++ b/web/config/routes.ts @@ -1,47 +1,255 @@ /** + * KRA - 路由配置 + * 对应 GVA 的路由结构 * @name umi 的路由配置 - * @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置 - * @param path path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。 - * @param component 配置 location 和 path 匹配后用于渲染的 React 组件路径。可以是绝对路径,也可以是相对路径,如果是相对路径,会从 src/pages 开始找起。 - * @param routes 配置子路由,通常在需要为多个路径增加 layout 组件时使用。 - * @param redirect 配置路由跳转 - * @param wrappers 配置路由组件的包装组件,通过包装组件可以为当前的路由组件组合进更多的功能。 比如,可以用于路由级别的权限校验 - * @param name 配置路由的标题,默认读取国际化文件 menu.ts 中 menu.xxxx 的值,如配置 name 为 login,则读取 menu.ts 中 menu.login 的取值作为标题 - * @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn, 注意去除风格后缀和大小写,如想要配置图标为 则取值应为 stepBackward 或 StepBackward,如想要配置图标为 则取值应为 user 或者 User * @doc https://umijs.org/docs/guides/routes */ export default [ + // 用户相关(无布局) { - path: "/user", + path: '/user', layout: false, routes: [ { - name: "login", - path: "/user/login", - component: "./user/login", + name: 'login', + path: '/user/login', + component: './user/login', }, ], }, + + // 数据库初始化(无布局) { - path: "/welcome", - name: "welcome", - icon: "smile", - component: "./Welcome", - }, - { - name: "admin", - icon: "crown", - path: "/admins", - access: "canAdmin", - component: "./admins", - }, - { - path: "/", - redirect: "/welcome", - }, - { - component: "404", + path: '/init', layout: false, - path: "./*", + component: './init', + }, + + // 错误页面(无布局) + { + path: '/error', + layout: false, + routes: [ + { path: '/error/403', component: './error/403' }, + { path: '/error/404', component: './error/404' }, + { path: '/error/500', component: './error/500' }, + { path: '/error/reload', component: './error/reload' }, + ], + }, + + // 仪表盘 + { + path: '/dashboard', + name: 'dashboard', + icon: 'dashboard', + component: './dashboard', + }, + + // 超级管理员 - 用户管理 + { + path: '/admin', + name: 'superAdmin', + icon: 'crown', + access: 'isAdmin', + 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: '/systemTools', + name: 'systemTools', + icon: 'tool', + access: 'isAdmin', + 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: '/plugin', + name: 'plugin', + icon: 'appstore', + routes: [ + { + path: '/plugin/announcement', + name: 'announcement', + icon: 'notification', + component: './plugin/announcement', + }, + { + path: '/plugin/email', + name: 'email', + icon: 'mail', + component: './plugin/email', + }, + ], + }, + + // 示例 + { + path: '/example', + name: 'example', + icon: 'experiment', + 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: '/person', + name: 'person', + icon: 'user', + component: './person', + hideInMenu: true, + }, + + // 关于 + { + path: '/about', + name: 'about', + icon: 'info-circle', + component: './about', + }, + + // 默认重定向 + { + path: '/', + redirect: '/dashboard', + }, + + // 404 + { + path: '*', + layout: false, + component: './error/404', }, ]; diff --git a/web/package.json b/web/package.json index ed91c35..367485f 100644 --- a/web/package.json +++ b/web/package.json @@ -31,11 +31,13 @@ "test:update": "npm run jest -- -u", "tsc": "tsc --noEmit" }, - "browserslist": ["defaults"], + "browserslist": [ + "defaults" + ], "dependencies": { + "@ant-design/charts": "^2.2.1", "@ant-design/icons": "^6.1.0", "@ant-design/pro-components": "^2.8.9", - "@ant-design/charts": "^2.2.1", "antd": "^6.0.0", "antd-style": "^3.7.0", "axios": "^1.8.2", @@ -44,6 +46,7 @@ "echarts": "^5.5.1", "echarts-for-react": "^3.0.2", "lodash": "^4.17.21", + "mitt": "^3.0.1", "nprogress": "^0.2.0", "qs": "^6.13.0", "react": "^19.1.0", @@ -56,6 +59,7 @@ }, "devDependencies": { "@ant-design/pro-cli": "^3.3.0", + "@biomejs/biome": "^2.1.1", "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", "@testing-library/dom": "^10.4.0", @@ -63,6 +67,7 @@ "@types/express": "^5.0.3", "@types/jest": "^30.0.0", "@types/lodash": "^4.17.13", + "@types/node": "^24.0.13", "@types/nprogress": "^0.2.3", "@types/qs": "^6.9.17", "@types/react": "^19.1.8", @@ -82,16 +87,22 @@ "ts-node": "^10.9.2", "typescript": "^5.6.3", "umi-presets-pro": "^2.0.3", - "umi-serve": "^1.9.11", - "@biomejs/biome": "^2.1.1", - "@types/node": "^24.0.13" + "umi-serve": "^1.9.11" }, "engines": { "node": ">=20.0.0" }, "create-umi": { - "ignoreScript": ["docker*", "functions*", "site", "generateMock"], - "ignoreDependencies": ["netlify*", "serverless"], + "ignoreScript": [ + "docker*", + "functions*", + "site", + "generateMock" + ], + "ignoreDependencies": [ + "netlify*", + "serverless" + ], "ignore": [ ".dockerignore", ".git", diff --git a/web/src/access.ts b/web/src/access.ts index 373d9fa..cd2f577 100644 --- a/web/src/access.ts +++ b/web/src/access.ts @@ -1,11 +1,129 @@ /** + * KRA - Access Control + * 权限控制配置 * @see https://umijs.org/docs/max/access#access - * */ -export default function access( - initialState: { currentUser?: API.CurrentUser } | undefined, -) { + */ + +export interface UserAuthority { + authorityId: string | number; + authorityName: string; + defaultRouter?: string; +} + +export interface CurrentUser { + uuid?: string; + userName?: string; + nickName?: string; + headerImg?: string; + authority?: UserAuthority; + authorities?: UserAuthority[]; + access?: string; + // 按钮权限列表 + buttons?: string[]; + // 菜单权限列表 + menus?: string[]; +} + +export interface InitialState { + currentUser?: CurrentUser; + settings?: any; + loading?: boolean; +} + +export default function access(initialState: InitialState | undefined) { const { currentUser } = initialState ?? {}; + const authority = currentUser?.authority; + const authorities = currentUser?.authorities || []; + const buttons = currentUser?.buttons || []; + const menus = currentUser?.menus || []; + + // 获取所有角色ID + const authorityIds = authorities.map((a) => + typeof a.authorityId === 'string' ? parseInt(a.authorityId, 10) : a.authorityId + ); + + // 当前角色ID + const currentAuthorityId = authority?.authorityId + ? (typeof authority.authorityId === 'string' + ? parseInt(authority.authorityId, 10) + : authority.authorityId) + : 0; + + // 是否为超级管理员 (authorityId = 888) + const isSuperAdmin = currentAuthorityId === 888 || authorityIds.includes(888); + + // 是否为管理员 (authorityId <= 1000 通常为管理员角色) + const isAdmin = isSuperAdmin || currentAuthorityId <= 1000; + return { - canAdmin: currentUser && currentUser.access === 'admin', + // 是否已登录 + isLoggedIn: !!currentUser, + + // 是否为超级管理员 + isSuperAdmin, + + // 是否为管理员 + isAdmin, + + // 兼容旧版 canAdmin + canAdmin: isAdmin, + + // 当前角色ID + currentAuthorityId, + + // 所有角色ID列表 + authorityIds, + + // 按钮权限列表 + buttons, + + // 菜单权限列表 + menus, + + // 检查是否有指定角色 + hasAuthority: (authorityId: number) => { + if (isSuperAdmin) return true; + return authorityIds.includes(authorityId) || currentAuthorityId === authorityId; + }, + + // 检查是否有指定按钮权限 + hasButton: (buttonKey: string) => { + if (isSuperAdmin) return true; + return buttons.includes(buttonKey); + }, + + // 检查是否有指定菜单权限 + hasMenu: (menuPath: string) => { + if (isSuperAdmin) return true; + return menus.includes(menuPath); + }, + + // 检查路由访问权限 + canAccessRoute: (path: string) => { + if (isSuperAdmin) return true; + // 公开路由 + const publicRoutes = ['/user/login', '/user/register', '/init', '/404', '/403', '/500']; + if (publicRoutes.some((route) => path.startsWith(route))) { + return true; + } + // 检查菜单权限 + if (menus.length > 0) { + return menus.some((menu) => path.startsWith(menu)); + } + // 默认允许访问(如果没有配置菜单权限) + return true; + }, + + // 检查是否有任一指定权限 + hasAnyAuthority: (ids: number[]) => { + if (isSuperAdmin) return true; + return ids.some((id) => authorityIds.includes(id) || currentAuthorityId === id); + }, + + // 检查是否有所有指定权限 + hasAllAuthorities: (ids: number[]) => { + if (isSuperAdmin) return true; + return ids.every((id) => authorityIds.includes(id) || currentAuthorityId === id); + }, }; } diff --git a/web/src/app.tsx b/web/src/app.tsx index 2c1dd16..a445ff4 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -1,129 +1,144 @@ -import { - AvatarDropdown, - AvatarName, - Footer, - Question, - SelectLang, -} from "@/components"; -import { createAdminService } from "@/services/index"; -import { Admin } from "@/services/kratos/admin/v1/index"; -import { LinkOutlined } from "@ant-design/icons"; -import type { Settings as LayoutSettings } from "@ant-design/pro-components"; -import { SettingDrawer } from "@ant-design/pro-components"; -import type { RequestConfig, RunTimeLayoutConfig } from "@umijs/max"; -import { history, Link } from "@umijs/max"; -import defaultSettings from "../config/defaultSettings"; -import { errorConfig } from "./requestErrorConfig"; +/** + * KRA - App Configuration + * 应用运行时配置 + */ +import { AvatarDropdown, AvatarName, Footer, Question } from '@/components'; +import { getUserInfo } from '@/services/kratos/user'; +import { getToken, removeToken } from '@/utils/auth'; +import { UserOutlined } from '@ant-design/icons'; +import type { Settings as LayoutSettings } from '@ant-design/pro-components'; +import { SettingDrawer } from '@ant-design/pro-components'; +import type { RequestConfig, RunTimeLayoutConfig } from '@umijs/max'; +import { history } from '@umijs/max'; +import { message } from 'antd'; +import defaultSettings from '../config/defaultSettings'; +import { errorConfig } from './requestErrorConfig'; -const isDev = process.env.NODE_ENV === "development"; -const isDevOrTest = isDev || process.env.CI; -const loginPath = "/user/login"; - -const adminService = createAdminService(); +const isDev = process.env.NODE_ENV === 'development'; +const loginPath = '/user/login'; +const initPath = '/init'; +const whiteList = [loginPath, initPath, '/error/403', '/error/404', '/error/500']; /** + * 获取初始状态 * @see https://umijs.org/docs/api/runtime-config#getinitialstate - * */ + */ export async function getInitialState(): Promise<{ settings?: Partial; currentUser?: API.CurrentUser; loading?: boolean; - fetchUserInfo?: () => Promise; + fetchUserInfo?: () => Promise; }> { - const fetchUserInfo = async () => { + const fetchUserInfo = async (): Promise => { try { - return await adminService.Current({}); + const token = getToken(); + if (!token) { + return undefined; + } + const res = await getUserInfo(); + if (res.data?.code === 0 && res.data?.data?.userInfo) { + const userInfo = res.data.data.userInfo; + return { + uuid: userInfo.uuid, + userName: userInfo.userName, + nickName: userInfo.nickName, + name: userInfo.nickName || userInfo.userName, + avatar: userInfo.headerImg, + headerImg: userInfo.headerImg, + authority: userInfo.authority, + authorities: userInfo.authorities, + // 按钮权限(如果后端返回) + buttons: res.data.data.buttons || [], + // 菜单权限(如果后端返回) + menus: res.data.data.menus || [], + }; + } + return undefined; } catch (error) { - history.push(loginPath); + console.error('获取用户信息失败:', error); + return undefined; } - return undefined; }; - // 如果不是登录页面,执行 + + // 如果是白名单页面,不获取用户信息 const { location } = history; - if ( - ![loginPath, "/user/register", "/user/register-result"].includes( - location.pathname - ) - ) { - const currentUser = await fetchUserInfo(); + if (whiteList.some((path) => location.pathname.startsWith(path))) { return { fetchUserInfo, - currentUser, settings: defaultSettings as Partial, }; } + + // 获取用户信息 + const currentUser = await fetchUserInfo(); + if (!currentUser) { + // 未登录,跳转到登录页 + history.push(loginPath); + } + return { fetchUserInfo, + currentUser, settings: defaultSettings as Partial, }; } -// ProLayout 支持的api https://procomponents.ant.design/components/layout -export const layout: RunTimeLayoutConfig = ({ - initialState, - setInitialState, -}) => { + +/** + * ProLayout 配置 + * @see https://procomponents.ant.design/components/layout + */ +export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => { return { - actionsRender: () => [ - , - , - ], + // 右上角操作区 + actionsRender: () => [], + + // 头像配置 avatarProps: { - src: initialState?.currentUser?.avatar, + src: initialState?.currentUser?.avatar || initialState?.currentUser?.headerImg, title: , - render: (_, avatarChildren) => ( - {avatarChildren} - ), + icon: , + render: (_, avatarChildren) => {avatarChildren}, }, + + // 水印 waterMarkProps: { - content: initialState?.currentUser?.name, + content: initialState?.currentUser?.nickName || initialState?.currentUser?.userName, }, + + // 页脚 footerRender: () =>