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 = () => { 更多} > {