diff --git a/web/src/app.tsx b/web/src/app.tsx index 1f95c91..924375f 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -3,6 +3,7 @@ * 应用运行时配置 */ import { AvatarDropdown, AvatarName, Footer } from '@/components'; +import CommandMenu from '@/components/CommandMenu'; import { getUserInfo } from '@/services/kratos/user'; import { getToken, removeToken } from '@/utils/auth'; import { @@ -12,12 +13,14 @@ import { SettingOutlined, UserOutlined, QuestionCircleOutlined, + SearchOutlined, } 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, Tooltip } from 'antd'; +import React, { useState } from 'react'; import defaultSettings from '../config/defaultSettings'; import { errorConfig } from './requestErrorConfig'; @@ -114,69 +117,69 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) = })); }; + // 打开设置 + const handleOpenSetting = () => { + const settingBtn = document.querySelector('.ant-pro-setting-drawer-handle'); + if (settingBtn) { + (settingBtn as HTMLElement).click(); + } + }; + + const iconStyle: React.CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + width: 32, + height: 32, + borderRadius: '50%', + border: '1px solid #d9d9d9', + cursor: 'pointer', + }; + return { // 先展开 settings,让后面的配置可以覆盖 ...initialState?.settings, // 右上角操作区 actionsRender: () => { - const isDark = initialState?.settings?.navTheme === 'realDark'; + const isDarkTheme = initialState?.settings?.navTheme === 'realDark'; return [ // 帮助文档 , + // 搜索 (Ctrl+K) + + window.dispatchEvent(new CustomEvent('openCommandMenu'))} + style={iconStyle} + > + + + , + // 系统设置 + + + + + , // 刷新 - + , // 切换主题 - - {isDark ? ( + + {isDarkTheme ? ( ) : ( @@ -188,7 +191,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) = // 头像配置 avatarProps: { - src: initialState?.currentUser?.avatar || initialState?.currentUser?.headerImg, + src: initialState?.currentUser?.avatar || initialState?.currentUser?.headerImg || '/default-avatar.svg', title: , icon: , render: (_, avatarChildren) => {avatarChildren}, @@ -231,25 +234,47 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) = return ( <> {children} - {isDev && ( - { - setInitialState((preInitialState) => ({ - ...preInitialState, - settings, - })); - }} - /> - )} + + { + setInitialState((preInitialState) => ({ + ...preInitialState, + settings, + })); + }} + /> > ); }, }; }; +// CommandMenu包装组件 +const CommandMenuWrapper: React.FC = () => { + const [open, setOpen] = useState(false); + + React.useEffect(() => { + const handleOpen = () => setOpen(true); + const handleKeyDown = (e: KeyboardEvent) => { + if (e.ctrlKey && e.key === 'k') { + e.preventDefault(); + setOpen(true); + } + }; + window.addEventListener('openCommandMenu', handleOpen); + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('openCommandMenu', handleOpen); + window.removeEventListener('keydown', handleKeyDown); + }; + }, []); + + return setOpen(false)} />; +}; + /** * 请求配置 * @see https://umijs.org/docs/max/request#配置 diff --git a/web/src/components/CommandMenu/index.tsx b/web/src/components/CommandMenu/index.tsx new file mode 100644 index 0000000..8c68ee4 --- /dev/null +++ b/web/src/components/CommandMenu/index.tsx @@ -0,0 +1,193 @@ +/** + * KRA - Command Menu Component + * 快捷搜索菜单,支持Ctrl+K快捷键 + */ +import React, { useState, useEffect, useMemo } from 'react'; +import { Modal, Input, List, Typography, Space, Tag } from 'antd'; +import { SearchOutlined, ArrowRightOutlined, BulbOutlined, LogoutOutlined } from '@ant-design/icons'; +import { history, useModel } from '@umijs/max'; +import { createStyles } from 'antd-style'; + +const { Text } = Typography; + +const useStyles = createStyles(({ token }) => ({ + searchInput: { + fontSize: 16, + padding: '12px 16px', + }, + categoryTitle: { + fontSize: 12, + fontWeight: 600, + color: token.colorTextSecondary, + marginTop: 8, + marginBottom: 4, + }, + listItem: { + padding: '8px 12px', + cursor: 'pointer', + borderRadius: 4, + '&:hover': { + backgroundColor: token.colorBgTextHover, + }, + }, +})); + +interface CommandMenuProps { + open: boolean; + onClose: () => void; +} + +interface MenuItem { + label: string; + path?: string; + action?: () => void; + icon?: React.ReactNode; +} + +interface MenuCategory { + label: string; + items: MenuItem[]; +} + +const CommandMenu: React.FC = ({ open, onClose }) => { + const { styles } = useStyles(); + const [searchValue, setSearchValue] = useState(''); + const { initialState, setInitialState } = useModel('@@initialState'); + + // 菜单项 + const menuCategories: MenuCategory[] = useMemo(() => { + const categories: MenuCategory[] = [ + { + label: '跳转', + items: [ + { label: '仪表盘', path: '/dashboard' }, + { label: '用户管理', path: '/admin/user' }, + { label: '角色管理', path: '/admin/authority' }, + { label: '菜单管理', path: '/admin/menu' }, + { label: 'API管理', path: '/admin/api' }, + { label: '操作日志', path: '/admin/operation' }, + { label: '字典管理', path: '/admin/dictionary' }, + { label: '参数配置', path: '/admin/params' }, + { label: '系统配置', path: '/systemTools/system' }, + { label: '个人信息', path: '/person' }, + { label: '关于', path: '/about' }, + ], + }, + { + label: '操作', + items: [ + { + label: '亮色主题', + icon: , + action: () => { + setInitialState((s) => ({ + ...s, + settings: { ...s?.settings, navTheme: 'light' }, + })); + }, + }, + { + label: '暗色主题', + icon: , + action: () => { + setInitialState((s) => ({ + ...s, + settings: { ...s?.settings, navTheme: 'realDark' }, + })); + }, + }, + { + label: '退出登录', + icon: , + action: () => { + history.push('/user/login'); + }, + }, + ], + }, + ]; + return categories; + }, [setInitialState]); + + // 过滤菜单 + const filteredCategories = useMemo(() => { + if (!searchValue) return menuCategories; + return menuCategories + .map((category) => ({ + ...category, + items: category.items.filter((item) => + item.label.toLowerCase().includes(searchValue.toLowerCase()) + ), + })) + .filter((category) => category.items.length > 0); + }, [menuCategories, searchValue]); + + // 处理点击 + const handleItemClick = (item: MenuItem) => { + if (item.path) { + history.push(item.path); + } else if (item.action) { + item.action(); + } + onClose(); + setSearchValue(''); + }; + + // 监听Ctrl+K快捷键 + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.ctrlKey && e.key === 'k') { + e.preventDefault(); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + return ( + + } + value={searchValue} + onChange={(e) => setSearchValue(e.target.value)} + className={styles.searchInput} + autoFocus + allowClear + /> + {filteredCategories.map((category, index) => ( + + {category.items.length > 0 && ( + <> + {category.label} + ( + handleItemClick(item)} + > + + {item.icon || } + {item.label} + + + )} + /> + > + )} + + ))} + + ); +}; + +export default CommandMenu; diff --git a/web/src/pages/about/index.tsx b/web/src/pages/about/index.tsx index 7d06168..5a9facc 100644 --- a/web/src/pages/about/index.tsx +++ b/web/src/pages/about/index.tsx @@ -134,7 +134,7 @@ const AboutPage: React.FC = () => { {/* 头部信息 */} - + @@ -166,7 +166,7 @@ const AboutPage: React.FC = () => { {/* 系统信息 */} - + Kratos Admin @@ -200,7 +200,7 @@ const AboutPage: React.FC = () => { {/* 技术栈 */} - + Go 1.21+ Kratos v2 @@ -215,7 +215,7 @@ const AboutPage: React.FC = () => { {/* 核心特性 */} - + {features.map((feature, index) => ( @@ -239,7 +239,7 @@ const AboutPage: React.FC = () => { {/* 鸣谢 */} - + Kratos Admin 的开发离不开以下优秀的开源项目: diff --git a/web/src/pages/dashboard/components/Notice.tsx b/web/src/pages/dashboard/components/Notice.tsx index 626bc3d..832d922 100644 --- a/web/src/pages/dashboard/components/Notice.tsx +++ b/web/src/pages/dashboard/components/Notice.tsx @@ -31,7 +31,7 @@ const Notice: React.FC = () => { 更多} > { { {links.map((link, index) => ( diff --git a/web/src/pages/person/index.tsx b/web/src/pages/person/index.tsx index 3dea611..d3aebb7 100644 --- a/web/src/pages/person/index.tsx +++ b/web/src/pages/person/index.tsx @@ -227,22 +227,22 @@ const PersonPage: React.FC = () => { - + - + - + - + @@ -281,7 +281,7 @@ const PersonPage: React.FC = () => { {/* 顶部个人信息卡片 */} - + @@ -329,7 +329,7 @@ const PersonPage: React.FC = () => { {/* 左侧信息栏 */} - + 基本信息 @@ -368,7 +368,7 @@ const PersonPage: React.FC = () => { - + 技能特长 @@ -383,7 +383,7 @@ const PersonPage: React.FC = () => { {/* 右侧内容区 */} - + diff --git a/web/src/pages/system/authority/index.tsx b/web/src/pages/system/authority/index.tsx index 630bdcd..f457be7 100644 --- a/web/src/pages/system/authority/index.tsx +++ b/web/src/pages/system/authority/index.tsx @@ -46,9 +46,10 @@ const AuthorityPage: React.FC = () => { const fetchTableData = async () => { setLoading(true); try { - const res = await getAuthorityList({ page: 1, pageSize: 1000 }); + const res = await getAuthorityList(); if (res.code === 0) { - setTableData(res.data?.list || []); + // GVA接口直接返回树形数组,不是分页格式 + setTableData(res.data || []); } } catch (error) { console.error('Failed to fetch authority list:', error); diff --git a/web/src/pages/system/user/index.tsx b/web/src/pages/system/user/index.tsx index ae9901a..b213096 100644 --- a/web/src/pages/system/user/index.tsx +++ b/web/src/pages/system/user/index.tsx @@ -84,9 +84,10 @@ const UserPage: React.FC = () => { const fetchAuthorityOptions = async () => { try { - const res = await getAuthorityList({ page: 1, pageSize: 1000 }); + const res = await getAuthorityList(); if (res.code === 0) { - const options = buildAuthorityOptions(res.data?.list || []); + // GVA接口直接返回树形数组,不是分页格式 + const options = buildAuthorityOptions(res.data || []); setAuthOptions(options); } } catch (error) { diff --git a/web/src/services/kratos/authority.ts b/web/src/services/kratos/authority.ts index 8caf97a..923b7f4 100644 --- a/web/src/services/kratos/authority.ts +++ b/web/src/services/kratos/authority.ts @@ -4,42 +4,63 @@ import request from '@/utils/request'; export interface Authority { - authorityId: string; + authorityId: number | string; authorityName: string; - parentId?: string; + parentId?: number | string; defaultRouter?: string; children?: Authority[]; dataAuthorityId?: Authority[]; } /** 获取角色列表 */ -export const getAuthorityList = (data: { page: number; pageSize: number }) => { - return request.post('/authority/getAuthorityList', data); +export const getAuthorityList = (data?: { page?: number; pageSize?: number }) => { + return request.post('/authority/getAuthorityList', data || {}); }; /** 删除角色 */ -export const deleteAuthority = (data: { authorityId: string }) => { - return request.post('/authority/deleteAuthority', data); +export const deleteAuthority = (data: { authorityId: number | string }) => { + return request.post('/authority/deleteAuthority', { authorityId: Number(data.authorityId) }); }; /** 创建角色 */ export const createAuthority = (data: Authority) => { - return request.post('/authority/createAuthority', data); + return request.post('/authority/createAuthority', { + ...data, + authorityId: Number(data.authorityId), + parentId: data.parentId ? Number(data.parentId) : 0, + }); }; /** 拷贝角色 */ -export const copyAuthority = (data: { authority: Authority; oldAuthorityId: string }) => { - return request.post('/authority/copyAuthority', data); +export const copyAuthority = (data: { authority: Authority; oldAuthorityId: number | string }) => { + return request.post('/authority/copyAuthority', { + authority: { + ...data.authority, + authorityId: Number(data.authority.authorityId), + parentId: data.authority.parentId ? Number(data.authority.parentId) : 0, + }, + oldAuthorityId: Number(data.oldAuthorityId), + }); }; /** 设置角色资源权限 */ -export const setDataAuthority = (data: { authorityId: string; dataAuthorityId: Authority[] }) => { - return request.post('/authority/setDataAuthority', data); +export const setDataAuthority = (data: { authorityId: number | string; dataAuthorityId: Authority[] }) => { + return request.post('/authority/setDataAuthority', { + authorityId: Number(data.authorityId), + dataAuthorityId: data.dataAuthorityId?.map(item => ({ + ...item, + authorityId: Number(item.authorityId), + })), + }); }; /** 修改角色 */ export const updateAuthority = (data: Authority) => { - return request.put('/authority/updateAuthority', data); + return request.put('/authority/updateAuthority', { + ...data, + authorityId: Number(data.authorityId), + parentId: data.parentId ? Number(data.parentId) : 0, + }); }; export default { diff --git a/web/src/services/kratos/authorityBtn.ts b/web/src/services/kratos/authorityBtn.ts index ba9e52c..5d134ae 100644 --- a/web/src/services/kratos/authorityBtn.ts +++ b/web/src/services/kratos/authorityBtn.ts @@ -5,13 +5,19 @@ import request from '@/utils/request'; import type { ApiResponse } from '@/types/api'; /** 获取角色按钮权限 */ -export const getAuthorityBtn = (data: { authorityId: string; menuID: number }) => { - return request.post>('/authorityBtn/getAuthorityBtn', data); +export const getAuthorityBtn = (data: { authorityId: number | string; menuID: number }) => { + return request.post>('/authorityBtn/getAuthorityBtn', { + ...data, + authorityId: Number(data.authorityId), + }); }; /** 设置角色按钮权限 */ -export const setAuthorityBtn = (data: { authorityId: string; menuID: number; selected: string[] }) => { - return request.post>('/authorityBtn/setAuthorityBtn', data); +export const setAuthorityBtn = (data: { authorityId: number | string; menuID: number; selected: string[] }) => { + return request.post>('/authorityBtn/setAuthorityBtn', { + ...data, + authorityId: Number(data.authorityId), + }); }; /** 检查是否可以移除按钮权限 */ diff --git a/web/src/services/kratos/casbin.ts b/web/src/services/kratos/casbin.ts index 32518c9..a9bbf51 100644 --- a/web/src/services/kratos/casbin.ts +++ b/web/src/services/kratos/casbin.ts @@ -10,13 +10,18 @@ export interface CasbinInfo { } /** 更新角色API权限 */ -export const updateCasbin = (data: { authorityId: string; casbinInfos: CasbinInfo[] }) => { - return request.post>('/casbin/updateCasbin', data); +export const updateCasbin = (data: { authorityId: number | string; casbinInfos: CasbinInfo[] }) => { + return request.post>('/casbin/updateCasbin', { + ...data, + authorityId: Number(data.authorityId), + }); }; /** 获取角色API权限列表 */ -export const getPolicyPathByAuthorityId = (data: { authorityId: string }) => { - return request.post>('/casbin/getPolicyPathByAuthorityId', data); +export const getPolicyPathByAuthorityId = (data: { authorityId: number | string }) => { + return request.post>('/casbin/getPolicyPathByAuthorityId', { + authorityId: Number(data.authorityId), + }); }; export default { diff --git a/web/src/services/kratos/menu.ts b/web/src/services/kratos/menu.ts index c414efc..f2352a4 100644 --- a/web/src/services/kratos/menu.ts +++ b/web/src/services/kratos/menu.ts @@ -44,13 +44,18 @@ export const getBaseMenuTree = () => { }; /** 添加菜单权限关联 */ -export const addMenuAuthority = (data: { menus: number[]; authorityId: string }) => { - return request.post('/menu/addMenuAuthority', data); +export const addMenuAuthority = (data: { menus: number[]; authorityId: number | string }) => { + return request.post('/menu/addMenuAuthority', { + ...data, + authorityId: Number(data.authorityId), + }); }; /** 获取菜单权限关联 */ -export const getMenuAuthority = (data: { authorityId: string }) => { - return request.post('/menu/getMenuAuthority', data); +export const getMenuAuthority = (data: { authorityId: number | string }) => { + return request.post('/menu/getMenuAuthority', { + authorityId: Number(data.authorityId), + }); }; /** 删除菜单 */ diff --git a/web/src/services/kratos/user.ts b/web/src/services/kratos/user.ts index d8a7a5e..4b319d0 100644 --- a/web/src/services/kratos/user.ts +++ b/web/src/services/kratos/user.ts @@ -48,8 +48,11 @@ export const getUserList = (data: PageParams) => { }; /** 设置用户权限 */ -export const setUserAuthority = (data: { uuid: string; authorityId: string }) => { - return request.post>('/user/setUserAuthority', data); +export const setUserAuthority = (data: { uuid: string; authorityId: string | number }) => { + return request.post>('/user/setUserAuthority', { + ...data, + authorityId: Number(data.authorityId), + }); }; /** 删除用户 */ @@ -73,8 +76,11 @@ export const setSelfSetting = (data: any) => { }; /** 设置用户多角色权限 */ -export const setUserAuthorities = (data: { ID: number; authorityIds: string[] }) => { - return request.post>('/user/setUserAuthorities', data); +export const setUserAuthorities = (data: { ID: number; authorityIds: (number | string)[] }) => { + return request.post>('/user/setUserAuthorities', { + ...data, + authorityIds: data.authorityIds.map(id => Number(id)), + }); }; /** 获取用户信息 */