mirror of https://github.com/msgbyte/tailchat
feat: 增加 QuickSwitcher
parent
3157a392aa
commit
3329382415
@ -0,0 +1,147 @@
|
||||
import { stopPropagation } from '@/utils/dom-helper';
|
||||
import { Input } from 'antd';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { t } from 'tailchat-shared';
|
||||
import { PortalAdd, PortalRemove } from './Portal';
|
||||
import _take from 'lodash/take';
|
||||
import clsx from 'clsx';
|
||||
import { useGlobalKeyDown } from '@/hooks/useGlobalKeyDown';
|
||||
import { isArrowDown, isArrowUp, isEnterHotkey } from '@/utils/hot-key';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
let currentQuickSwitcherKey: number | null = null;
|
||||
|
||||
interface QuickActionContext {
|
||||
navigate: (url: string) => void;
|
||||
}
|
||||
|
||||
interface QuickAction {
|
||||
key: string;
|
||||
label: string;
|
||||
action: (context: QuickActionContext) => void;
|
||||
}
|
||||
|
||||
const builtinActions: QuickAction[] = [
|
||||
{
|
||||
key: 'personal',
|
||||
label: t('个人主页'),
|
||||
action({ navigate }) {
|
||||
navigate('/main/personal/friends');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'plugins',
|
||||
label: t('插件中心'),
|
||||
action({ navigate }) {
|
||||
navigate('/main/personal/plugins');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function useQuickSwitcherActionContext(): QuickActionContext {
|
||||
const history = useHistory();
|
||||
return {
|
||||
navigate: (url) => {
|
||||
history.push(url);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const QuickSwitcher: React.FC = React.memo(() => {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const actionContext = useQuickSwitcherActionContext();
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (!currentQuickSwitcherKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
PortalRemove(currentQuickSwitcherKey);
|
||||
currentQuickSwitcherKey = null;
|
||||
}, []);
|
||||
|
||||
const filteredActions = useMemo(() => {
|
||||
return _take(
|
||||
builtinActions.filter((action) => action.label.includes(keyword)),
|
||||
5
|
||||
);
|
||||
}, [keyword]);
|
||||
|
||||
useGlobalKeyDown((e) => {
|
||||
if (isArrowUp(e)) {
|
||||
const newIndex = selectedIndex - 1;
|
||||
setSelectedIndex(
|
||||
newIndex >= 0 ? newIndex : filteredActions.length + newIndex
|
||||
);
|
||||
} else if (isArrowDown(e)) {
|
||||
setSelectedIndex((selectedIndex + 1) % filteredActions.length);
|
||||
}
|
||||
|
||||
if (isEnterHotkey(e)) {
|
||||
const selectedAction = filteredActions[selectedIndex];
|
||||
typeof selectedAction.action === 'function' &&
|
||||
selectedAction.action(actionContext);
|
||||
handleClose();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed left-0 right-0 top-0 bottom-0 bg-black bg-opacity-60 flex justify-center"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<div
|
||||
className="modal-inner bg-content-light dark:bg-content-dark rounded overflow-auto relative p-4"
|
||||
style={{
|
||||
marginTop: '20vh',
|
||||
height: 'fit-content',
|
||||
maxHeight: '60vh',
|
||||
width: '60vw',
|
||||
maxWidth: '1280px',
|
||||
}}
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
<Input
|
||||
className="mb-1"
|
||||
autoFocus={true}
|
||||
placeholder={t('快速搜索、跳转')}
|
||||
size="large"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
/>
|
||||
|
||||
{filteredActions.map((action, i) => (
|
||||
<div
|
||||
key={action.key}
|
||||
className={clsx('truncate px-2 py-1 rounded', {
|
||||
'bg-black bg-opacity-20 dark:bg-white dark:bg-opacity-20':
|
||||
selectedIndex === i,
|
||||
})}
|
||||
>
|
||||
<div className="text-lg">{action.label}</div>
|
||||
<div
|
||||
className={clsx('opacity-0 text-gray-400 text-xs', {
|
||||
'opacity-100': selectedIndex === i,
|
||||
})}
|
||||
>
|
||||
{action.key}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
QuickSwitcher.displayName = 'QuickSwitcher';
|
||||
|
||||
/**
|
||||
* 打开快速开关
|
||||
*/
|
||||
export function openQuickSwitcher() {
|
||||
if (typeof currentQuickSwitcherKey === 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
currentQuickSwitcherKey = PortalAdd(<QuickSwitcher />);
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { useUpdateRef } from 'tailchat-shared';
|
||||
|
||||
/**
|
||||
* keydown hooks
|
||||
* 仅接受最初的函数
|
||||
*/
|
||||
export function useGlobalKeyDown(fn: (e: KeyboardEvent) => void) {
|
||||
const fnRef = useUpdateRef(fn);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
typeof fnRef.current === 'function' && fnRef.current(e);
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { openQuickSwitcher } from '@/components/QuickSwitcher';
|
||||
import { useGlobalKeyDown } from '@/hooks/useGlobalKeyDown';
|
||||
import { isQuickSwitcher } from '@/utils/hot-key';
|
||||
|
||||
/**
|
||||
* 全局快捷键
|
||||
*/
|
||||
export function useShortcuts() {
|
||||
useGlobalKeyDown((e) => {
|
||||
if (isQuickSwitcher(e)) {
|
||||
// 显示快速开关
|
||||
openQuickSwitcher();
|
||||
}
|
||||
});
|
||||
}
|
@ -1,3 +1,9 @@
|
||||
import { isHotkey } from 'is-hotkey';
|
||||
|
||||
export const isEnterHotkey = isHotkey('enter');
|
||||
|
||||
export const isQuickSwitcher = isHotkey('mod+k');
|
||||
|
||||
export const isArrowUp = isHotkey('up');
|
||||
|
||||
export const isArrowDown = isHotkey('down');
|
||||
|
Loading…
Reference in New Issue