diff --git a/src/components/Application/src/search/useMenuSearch.ts b/src/components/Application/src/search/useMenuSearch.ts index 4314eb5f..ac15a2f4 100644 --- a/src/components/Application/src/search/useMenuSearch.ts +++ b/src/components/Application/src/search/useMenuSearch.ts @@ -1,5 +1,5 @@ import { cloneDeep } from 'lodash-es'; -import { ref, onBeforeUnmount, onBeforeMount, unref, Ref } from 'vue'; +import { ref, onBeforeMount, unref, Ref } from 'vue'; import { useI18n } from '/@/hooks/web/useI18n'; import { getMenus } from '/@/router/menus'; import type { Menu } from '/@/router/types'; @@ -7,6 +7,7 @@ import { filter, forEach } from '/@/utils/helper/treeHelper'; import { useDebounce } from '/@/hooks/core/useDebounce'; import { useGo } from '/@/hooks/web/usePage'; import { useScrollTo } from '/@/hooks/event/useScrollTo'; +import { useKeyPress } from '/@/hooks/event/useKeyPress'; export interface SearchResult { name: string; @@ -50,12 +51,6 @@ export function useMenuSearch(refs: Ref, scrollWrap: Ref, forEach(menuList, (item) => { item.name = t(item.name); }); - - document.addEventListener('keydown', registerKeyDown); - }); - - onBeforeUnmount(() => { - document.removeEventListener('keydown', registerKeyDown); }); function search(e: ChangeEvent) { @@ -151,8 +146,8 @@ export function useMenuSearch(refs: Ref, scrollWrap: Ref, emit('close'); } - function registerKeyDown(e: KeyboardEvent) { - const keyCode = window.event ? e.keyCode : e.which; + useKeyPress(['enter', 'up', 'down'], (events) => { + const keyCode = events.keyCode; switch (keyCode) { case KeyCodeEnum.UP: handleUp(); @@ -167,7 +162,7 @@ export function useMenuSearch(refs: Ref, scrollWrap: Ref, handleClose(); break; } - } + }); return { handleSearch, searchResult, keyword, activeIndex, handleMouseenter, handleEnter }; } diff --git a/src/hooks/core/useEffect.ts b/src/hooks/core/useEffect.ts index 7c85565b..176c78a0 100644 --- a/src/hooks/core/useEffect.ts +++ b/src/hooks/core/useEffect.ts @@ -1,8 +1,10 @@ -import { WatchOptions } from 'vue'; import { watch } from 'vue'; import { isFunction } from '/@/utils/is'; -export const useEffect = (effectHandler: Fn, dependencies: any[]) => { +export function useEffect( + effectHandler: (deps: T[], prevDeps?: T[]) => () => void, + dependencies: T[] +) { return watch( dependencies, (changedDependencies, prevDependencies, onCleanUp) => { @@ -11,6 +13,6 @@ export const useEffect = (effectHandler: Fn, dependencies: any[]) => { onCleanUp(effectCleaner); } }, - { immediate: true, deep: true } as WatchOptions + { immediate: true, deep: true } ); -}; +} diff --git a/src/hooks/core/useLockFn.ts b/src/hooks/core/useLockFn.ts new file mode 100644 index 00000000..6da611fb --- /dev/null +++ b/src/hooks/core/useLockFn.ts @@ -0,0 +1,19 @@ +import { ref, unref } from 'vue'; + +export function useLockFn

( + fn: (...args: P) => Promise +) { + const lockRef = ref(false); + return async function (...args: P) { + if (unref(lockRef)) return; + lockRef.value = true; + try { + const ret = await fn(...args); + lockRef.value = false; + return ret; + } catch (e) { + lockRef.value = false; + throw e; + } + }; +} diff --git a/src/hooks/core/useModel.ts b/src/hooks/core/useModel.ts deleted file mode 100644 index 572d6378..00000000 --- a/src/hooks/core/useModel.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { toRef, Ref, reactive, customRef, SetupContext, watch, UnwrapRef } from 'vue'; - -export type ModelProps = Readonly< - { [props: string]: any } & { - modelValue?: U; - } ->; - -export function useModel( - props: ModelProps, - context: SetupContext, - callback?: (val: T | undefined, internalState: { value: UnwrapRef }) => any -) { - const outerModel: Ref = toRef(props, 'modelValue'); - const internalState = reactive({ - value: props.modelValue, - }); - - const internalModel = customRef | undefined>((track, trigger) => { - return { - get() { - track(); - return internalState.value; - }, - set(newVal) { - if (internalState.value === newVal) return; - internalState.value = newVal; - context.emit('update:modelValue', newVal); - trigger(); - }, - }; - }); - - watch(outerModel, (val, oldVal) => { - if (val === oldVal || val === internalState.value) return; - if (callback) { - callback(val, internalState); - return; - } - internalState.value = val as UnwrapRef | undefined; - }); - - return { - internalState, - internalModel, - }; -} diff --git a/src/hooks/core/useState.ts b/src/hooks/core/useState.ts new file mode 100644 index 00000000..7c4f3f83 --- /dev/null +++ b/src/hooks/core/useState.ts @@ -0,0 +1,58 @@ +import { isObject } from '@vue/shared'; +import { reactive, Ref, ref, readonly } from 'vue'; +import { isFunction } from '/@/utils/is'; + +type State = ((s: T) => T) | T; +type Dispatch = (t: T) => void; + +type DispatchState = Dispatch>; + +type ResultState = Readonly>; + +export function useState( + initialState: (() => T) | T +): [ResultState, DispatchState]; + +export function useState( + initialState: (() => T) | T +): [ResultState, DispatchState]; + +export function useState( + initialState: (() => T) | T +): [ResultState, DispatchState]; + +export function useState( + initialState: (() => T) | T +): [ResultState, DispatchState]; + +export function useState( + initialState: (() => T) | T +): [ResultState, DispatchState]; + +export function useState( + initialState: (() => T) | T +): [Readonly, DispatchState]; + +export function useState( + initialState: (() => T) | T +): [Readonly, DispatchState]; + +export function useState(initialState: (() => T) | T): [ResultState | T, DispatchState] { + if (isFunction(initialState)) { + initialState = (initialState as Fn)(); + } + + if (isObject(initialState)) { + const state = reactive({ data: initialState }) as any; + const setState = (newState: T) => { + state.data = newState; + }; + return [readonly(state), setState]; + } else { + const state = ref(initialState) as any; + const setState = (newState: T) => { + state.value = newState; + }; + return [readonly(state), setState]; + } +} diff --git a/src/hooks/core/useToggle.ts b/src/hooks/core/useToggle.ts deleted file mode 100644 index eda88d16..00000000 --- a/src/hooks/core/useToggle.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ref, watch, Ref, SetupContext } from 'vue'; - -export function useToggle(internalModel: Ref, { emit }: SetupContext) { - const isActive = ref(!!internalModel.value); - const isToggled = ref(false); - watch(internalModel, (val) => { - isActive.value = !!val; - }); - watch(isActive, (value) => { - !!value !== !!internalModel.value && emit('onUpdate:modelValue', value); - }); - function toggleIt() { - isToggled.value = !isToggled.value; - } - return { - isActive, - toggleIt, - isToggled, - }; -} diff --git a/src/hooks/event/useKeyPress.ts b/src/hooks/event/useKeyPress.ts new file mode 100644 index 00000000..72338d67 --- /dev/null +++ b/src/hooks/event/useKeyPress.ts @@ -0,0 +1,172 @@ +// https://ahooks.js.org/zh-CN/hooks/dom/use-key-press + +import type { Ref } from 'vue'; +import { onBeforeUnmount, onMounted, unref } from 'vue'; +import { noop } from '/@/utils'; +import { isFunction, isString, isNumber, isArray } from '/@/utils/is'; + +export type KeyPredicate = (event: KeyboardEvent) => boolean; +export type keyType = KeyboardEvent['keyCode'] | KeyboardEvent['key']; +export type KeyFilter = keyType | keyType[] | ((event: KeyboardEvent) => boolean); +export type EventHandler = (event: KeyboardEvent) => void; + +export type keyEvent = 'keydown' | 'keyup'; + +export type TargetElement = HTMLElement | Element | Document | Window; +export type Target = Ref; + +export type EventOption = { + events?: keyEvent[]; + target?: Target; +}; + +const defaultEvents: keyEvent[] = ['keydown']; + +// 键盘事件 keyCode 别名 +const aliasKeyCodeMap: Record = { + esc: 27, + tab: 9, + enter: 13, + space: 32, + up: 38, + left: 37, + right: 39, + down: 40, + delete: [8, 46], +}; + +// 键盘事件 key 别名 +const aliasKeyMap: Record = { + esc: 'Escape', + tab: 'Tab', + enter: 'Enter', + space: ' ', + // IE11 uses key names without `Arrow` prefix for arrow keys. + up: ['Up', 'ArrowUp'], + left: ['Left', 'ArrowLeft'], + right: ['Right', 'ArrowRight'], + down: ['Down', 'ArrowDown'], + delete: ['Backspace', 'Delete'], +}; + +// 修饰键 +const modifierKey: Record boolean> = { + ctrl: (event: KeyboardEvent) => event.ctrlKey, + shift: (event: KeyboardEvent) => event.shiftKey, + alt: (event: KeyboardEvent) => event.altKey, + meta: (event: KeyboardEvent) => event.metaKey, +}; + +/** + * 判断按键是否激活 + * @param [event: KeyboardEvent]键盘事件 + * @param [keyFilter: any] 当前键 + * @returns Boolean + */ +function genFilterKey(event: any, keyFilter: any) { + // 浏览器自动补全 input 的时候,会触发 keyDown、keyUp 事件,但此时 event.key 等为空 + if (!event.key) { + return false; + } + + // 数字类型直接匹配事件的 keyCode + if (isNumber(keyFilter)) { + return event.keyCode === keyFilter; + } + // 字符串依次判断是否有组合键 + const genArr = keyFilter.split('.'); + let genLen = 0; + for (const key of genArr) { + // 组合键 + const genModifier = modifierKey[key]; + // key 别名 + const aliasKey = aliasKeyMap[key]; + // keyCode 别名 + const aliasKeyCode = aliasKeyCodeMap[key]; + /** + * 满足以上规则 + * 1. 自定义组合键别名 + * 2. 自定义 key 别名 + * 3. 自定义 keyCode 别名 + * 4. 匹配 key 或 keyCode + */ + if ( + (genModifier && genModifier(event)) || + (aliasKey && isArray(aliasKey) ? aliasKey.includes(event.key) : aliasKey === event.key) || + (aliasKeyCode && isArray(aliasKeyCode) + ? aliasKeyCode.includes(event.keyCode) + : aliasKeyCode === event.keyCode) || + event.key.toUpperCase() === key.toUpperCase() + ) { + genLen++; + } + } + return genLen === genArr.length; +} + +/** + * 键盘输入预处理方法 + */ +function genKeyFormat(keyFilter: any): KeyPredicate { + if (isFunction(keyFilter)) { + return keyFilter; + } + if (isString(keyFilter) || isNumber(keyFilter)) { + return (event: KeyboardEvent) => genFilterKey(event, keyFilter); + } + if (isArray(keyFilter)) { + return (event: KeyboardEvent) => keyFilter.some((item: any) => genFilterKey(event, item)); + } + return keyFilter ? () => true : () => false; +} + +export function useKeyPress( + keyFilter: KeyFilter, + eventHandler: EventHandler = noop, + option: EventOption = {} +) { + const { events = defaultEvents, target } = option; + + let el: TargetElement | null | undefined; + + function handler(event: any) { + const genGuard: KeyPredicate = genKeyFormat(keyFilter); + if (genGuard(event)) { + return eventHandler(event); + } + } + + onMounted(() => { + el = getTargetElement(target, window); + if (!el) return; + + for (const eventName of events) { + el.addEventListener(eventName, handler); + } + }); + + onBeforeUnmount(() => { + if (!el) return; + for (const eventName of events) { + el.removeEventListener(eventName, handler); + } + }); +} + +export function getTargetElement( + target?: Target, + defaultElement?: TargetElement +): TargetElement | undefined | null { + if (!target) { + return defaultElement; + } + + let targetElement: TargetElement | undefined | null; + + if (isFunction(target)) { + targetElement = target(); + } else { + targetElement = unref(target); + } + return targetElement; +} diff --git a/src/hooks/web/useScript.ts b/src/hooks/web/useScript.ts index ce790574..20333fbe 100644 --- a/src/hooks/web/useScript.ts +++ b/src/hooks/web/useScript.ts @@ -16,7 +16,7 @@ export function useScript(opts: ScriptOptions) { isLoading.value = false; success.value = true; error.value = false; - resolve(); + resolve(''); }; script.onerror = function (err) {