Skip to content

Commit

Permalink
feat(projects): add menu functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Ohh-889 committed Sep 7, 2024
1 parent 79c1ae1 commit 0b14deb
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 6 deletions.
30 changes: 30 additions & 0 deletions src/layouts/modules/global-search/components/SearchFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import classNames from 'classnames';
import { Divider } from 'antd';
import style from './footer.module.scss';

const SearchFooter = () => {
const { t } = useTranslation();

return (
<>
<Divider className="my-2px" />
<div className="h-44px flex-center gap-14px">
<span className="flex-y-center">
<IconMdiKeyboardReturn className={classNames(style['operate-shadow'], style['operate-item'])} />
<span>{t('common.confirm')}</span>
</span>
<span className="flex-y-center">
<IconMdiArrowUpThin className={classNames([style['operate-shadow'], style['operate-item']])} />
<IconMdiArrowDownThin className={classNames(style['operate-shadow'], style['operate-item'])} />
<span>{t('common.switch')}</span>
</span>
<span className="flex-y-center">
<IconMdiKeyboardEsc className={classNames(style['operate-shadow'], style['operate-item'])} />
<span>{t('common.close')}</span>
</span>
</div>
</>
);
};

export default memo(SearchFooter);
160 changes: 160 additions & 0 deletions src/layouts/modules/global-search/components/SearchModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import classNames from 'classnames';
import { useDebounceFn, useKeyPress } from 'ahooks';
import { getIsMobile } from '@/store/slice/app';
import SearchFooter from './SearchFooter';
import SearchResult from './SearchResult';

interface Props {
show: boolean;
onClose: () => void;
}

/**
* Transform menu to searchMenus
*
* @param menus - menus
* @param treeMap
*/
function transformMenuToSearchMenus(menus: App.Global.Menu[], treeMap: App.Global.Menu[] = []) {
if (menus && menus.length === 0) return [];
return menus.reduce((acc, cur) => {
if (!cur.children) {
acc.push(cur);
}
if (cur.children && cur.children.length > 0) {
transformMenuToSearchMenus(cur.children, treeMap);
}
return acc;
}, treeMap);
}

const SearchModal: FC<Props> = memo(({ show, onClose }) => {
const isMobile = useAppSelector(getIsMobile);

const { t } = useTranslation();

const { allMenus } = useMixMenuContext();

const [keyword, setKeyword] = useState<string>('');

const [resultOptions, setResultOptions] = useState<App.Global.Menu[]>([]);

const [activeRouteName, setActiveRouteName] = useState<string>('');

const searchMenus = useMemo(() => transformMenuToSearchMenus(allMenus), [allMenus]);

function handleClose() {
// handle with setTimeout to prevent user from seeing some operations
setTimeout(() => {
onClose();
setResultOptions([]);
setKeyword('');
}, 200);
}

function search() {
const result = searchMenus.filter(menu => {
const trimKeyword = keyword.toLocaleLowerCase().trim();
return trimKeyword && menu.title?.includes(trimKeyword);
});
const activeName = result[0]?.key ?? '';

setResultOptions(result);
setActiveRouteName(activeName);
}

/** key up */
function handleUp() {
handleKeyPress(-1); // 方向 -1 表示向上
}

/** key down */
function handleDown() {
handleKeyPress(1); // 方向 1 表示向下
}

function getActivePathIndex() {
return resultOptions.findIndex(item => item.key === activeRouteName);
}

function handleKeyPress(direction: 1 | -1) {
const { length } = resultOptions;
if (length === 0) return;

const index = getActivePathIndex();
if (index === -1) return;

const activeIndex = (index + direction + length) % length; // 确保 index 在范围内循环
const activeKey = resultOptions[activeIndex].key;

setActiveRouteName(activeKey);
}

const router = useRouterPush();

const handleSearch = useDebounceFn(search, { wait: 300 });

/** key enter */
function handleEnter() {
if (resultOptions.length === 0 || activeRouteName === '') return;
handleClose();
router.menuPush(activeRouteName);
}

useKeyPress('Escape', handleClose);
useKeyPress('Enter', handleEnter);
useKeyPress('uparrow', handleUp);
useKeyPress('downarrow', handleDown);

return (
<AModal
footer={isMobile ? null : <SearchFooter />}
styles={{ content: { paddingBottom: 0, height: isMobile ? '100vh' : '100%' } }}
style={isMobile ? { margin: 0, padding: 0, maxWidth: '100%' } : undefined}
height={isMobile ? '100%' : 400}
open={show}
width={isMobile ? '100%' : 630}
className={classNames({ 'top-0px rounded-0': isMobile })}
onCancel={onClose}
closable={false}
>
<ASpace.Compact className="w-full">
<AInput
allowClear
onInput={handleSearch.run}
prefix={<IconUilSearch className="text-15px text-#c2c2c2" />}
placeholder={t('common.keywordSearch')}
onChange={e => setKeyword(e.target.value)}
value={keyword}
/>
{isMobile && (
<AButton
onClick={handleClose}
type="primary"
ghost
>
{t('common.cancel')}
</AButton>
)}
</ASpace.Compact>

<div className="mt-20px">
{resultOptions.length === 0 ? (
<AEmpty />
) : (
resultOptions.map(item => (
<SearchResult
enter={handleEnter}
key={item.key}
active={item.key === activeRouteName}
setActiveRouteName={setActiveRouteName}
menu={item}
/>
))
)}
</div>
</AModal>
);
});

export default SearchModal;
35 changes: 35 additions & 0 deletions src/layouts/modules/global-search/components/SearchResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import classNames from 'classnames';

interface Props {
menu: App.Global.Menu;
active: boolean;
setActiveRouteName: (name: string) => void;
enter: () => void;
}

const SearchResult: FC<Props> = memo(({ menu, active, setActiveRouteName, enter }) => {
function handleMouseEnter() {
setActiveRouteName(menu.key);
}

return (
<div
className={classNames(
'mt-8px h-56px flex-y-center cursor-pointer justify-between rounded-4px bg-#e5e7eb px-14px dark:bg-dark',
{ 'bg-primary': active },
{ 'text-#fff': active }
)}
onMouseEnter={handleMouseEnter}
onClick={enter}
>
<span className="ml-5px flex-1">
{menu.icon}
{menu.label}
</span>

<IconAntDesignEnterOutlined />
</div>
);
});

export default SearchResult;
10 changes: 10 additions & 0 deletions src/layouts/modules/global-search/components/footer.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.operate-shadow {
box-shadow:
inset 0 -2px #cdcde6,
inset 0 0 1px 1px #fff,
0 1px 2px 1px #1e235a66;
}

.operate-item {
--uno: mr-6px p-2px text-20px;
}
28 changes: 22 additions & 6 deletions src/layouts/modules/global-search/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import { useBoolean } from 'ahooks';
import { Suspense } from 'react';

const SearchModal = lazy(() => import('./components/SearchModal'));

const GlobalSearch = memo(() => {
const { t } = useTranslation();

const [show, { toggle, setFalse }] = useBoolean();

return (
<ButtonIcon
className="px-12px"
tooltipContent={t('common.search')}
>
<IconUilSearch />
</ButtonIcon>
<>
<ButtonIcon
className="px-12px"
tooltipContent={t('common.search')}
onClick={toggle}
>
<IconUilSearch />
</ButtonIcon>
<Suspense fallback={null}>
<SearchModal
onClose={setFalse}
show={show}
/>
</Suspense>
</>
);
});

Expand Down
8 changes: 8 additions & 0 deletions src/types/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
// Generated by unplugin-auto-import
export {}
declare global {
const AButton: typeof import('antd')['Button']
const ACard: typeof import('antd')['Card']
const ACheckbox: typeof import('antd')['Checkbox']
const ACol: typeof import('antd')['Col']
const AColorPicker: typeof import('antd')['ColorPicker']
const AConfigProvider: typeof import('antd')['ConfigProvider']
const ADivider: typeof import('antd')['Divider']
const AEmpty: typeof import('antd')['Empty']
const AFlex: typeof import('antd')['Flex']
const AInput: typeof import('antd')['Input']
const AMenu: typeof import('antd')['Menu']
const AModal: typeof import('antd')['Modal']
const ARow: typeof import('antd')['Row']
const ASpace: typeof import('antd')['Space']
const AStatistic: typeof import('antd')['Statistic']
Expand All @@ -29,6 +32,7 @@ declare global {
const ExceptionBase: typeof import('../components/stateless/common/ExceptionBase')['default']
const FullScreen: typeof import('../components/stateless/common/FullScreen')['default']
const GlobalLoading: typeof import('../components/stateless/common/GlobalLoading')['default']
const IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined.tsx')['default']
const IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined.tsx')['default']
const IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined.tsx')['default']
const IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen.tsx')['default']
Expand All @@ -39,7 +43,11 @@ declare global {
const IconIcRoundSearch: typeof import('~icons/ic/round-search.tsx')['default']
const IconLocalBanner: typeof import('~icons/local/banner.tsx')['default']
const IconLocalLogo: typeof import('~icons/local/logo.tsx')['default']
const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin.tsx')['default']
const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin.tsx')['default']
const IconMdiDrag: typeof import('~icons/mdi/drag.tsx')['default']
const IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc.tsx')['default']
const IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return.tsx')['default']
const IconMdiRefresh: typeof import('~icons/mdi/refresh.tsx')['default']
const IconUilSearch: typeof import('~icons/uil/search.tsx')['default']
const LangSwitch: typeof import('../components/stateful/LangSwitch')['default']
Expand Down

0 comments on commit 0b14deb

Please sign in to comment.