feat: user-dropdown support `hover` trigger (#5143)
* feat: user-dropdown support `hover` trigger * fix: modified type declarationdev-v5
parent
7581fb381f
commit
f446cbf9e5
|
@ -25,6 +25,7 @@
|
||||||
"@vben/stores": "workspace:*",
|
"@vben/stores": "workspace:*",
|
||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"@vben/utils": "workspace:*",
|
"@vben/utils": "workspace:*",
|
||||||
|
"@vueuse/core": "catalog:",
|
||||||
"vue": "catalog:",
|
"vue": "catalog:",
|
||||||
"vue-router": "catalog:",
|
"vue-router": "catalog:",
|
||||||
"watermark-js-plus": "catalog:"
|
"watermark-js-plus": "catalog:"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export * from './use-app-config';
|
export * from './use-app-config';
|
||||||
export * from './use-content-maximize';
|
export * from './use-content-maximize';
|
||||||
export * from './use-design-tokens';
|
export * from './use-design-tokens';
|
||||||
|
export * from './use-hover-toggle';
|
||||||
export * from './use-pagination';
|
export * from './use-pagination';
|
||||||
export * from './use-refresh';
|
export * from './use-refresh';
|
||||||
export * from './use-tabs';
|
export * from './use-tabs';
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import type { Arrayable, MaybeElementRef } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import { isFunction } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useMouseInElement } from '@vueuse/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监测鼠标是否在元素内部,如果在元素内部则返回 true,否则返回 false
|
||||||
|
* @param refElement 所有需要检测的元素。如果提供了一个数组,那么鼠标在任何一个元素内部都会返回 true
|
||||||
|
* @param delay 延迟更新状态的时间
|
||||||
|
* @returns 返回一个数组,第一个元素是一个 ref,表示鼠标是否在元素内部,第二个元素是一个控制器,可以通过 enable 和 disable 方法来控制监听器的启用和禁用
|
||||||
|
*/
|
||||||
|
export function useHoverToggle(
|
||||||
|
refElement: Arrayable<MaybeElementRef>,
|
||||||
|
delay: (() => number) | number = 500,
|
||||||
|
) {
|
||||||
|
const isOutsides: Array<Ref<boolean>> = [];
|
||||||
|
const value = ref(false);
|
||||||
|
const timer = ref<ReturnType<typeof setTimeout> | undefined>();
|
||||||
|
const refs = Array.isArray(refElement) ? refElement : [refElement];
|
||||||
|
refs.forEach((refEle) => {
|
||||||
|
const listener = useMouseInElement(refEle, { handleOutside: true });
|
||||||
|
isOutsides.push(listener.isOutside);
|
||||||
|
});
|
||||||
|
const isOutsideAll = computed(() => isOutsides.every((v) => v.value));
|
||||||
|
|
||||||
|
function setValueDelay(val: boolean) {
|
||||||
|
timer.value && clearTimeout(timer.value);
|
||||||
|
timer.value = setTimeout(
|
||||||
|
() => {
|
||||||
|
value.value = val;
|
||||||
|
timer.value = undefined;
|
||||||
|
},
|
||||||
|
isFunction(delay) ? delay() : delay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const watcher = watch(
|
||||||
|
isOutsideAll,
|
||||||
|
(val) => {
|
||||||
|
setValueDelay(!val);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const controller = {
|
||||||
|
enable() {
|
||||||
|
watcher.resume();
|
||||||
|
},
|
||||||
|
disable() {
|
||||||
|
watcher.pause();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
timer.value && clearTimeout(timer.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [value, controller] as [typeof value, typeof controller];
|
||||||
|
}
|
|
@ -2,8 +2,9 @@
|
||||||
import type { AnyFunction } from '@vben/types';
|
import type { AnyFunction } from '@vben/types';
|
||||||
|
|
||||||
import type { Component } from 'vue';
|
import type { Component } from 'vue';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useHoverToggle } from '@vben/hooks';
|
||||||
import { LockKeyhole, LogOut } from '@vben/icons';
|
import { LockKeyhole, LogOut } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { preferences, usePreferences } from '@vben/preferences';
|
import { preferences, usePreferences } from '@vben/preferences';
|
||||||
|
@ -53,6 +54,10 @@ interface Props {
|
||||||
* 文本
|
* 文本
|
||||||
*/
|
*/
|
||||||
text?: string;
|
text?: string;
|
||||||
|
/** 触发方式 */
|
||||||
|
trigger?: 'both' | 'click' | 'hover';
|
||||||
|
/** hover触发时,延迟响应的时间 */
|
||||||
|
hoverDelay?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
|
@ -67,10 +72,11 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
showShortcutKey: true,
|
showShortcutKey: true,
|
||||||
tagText: '',
|
tagText: '',
|
||||||
text: '',
|
text: '',
|
||||||
|
trigger: 'click',
|
||||||
|
hoverDelay: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{ logout: [] }>();
|
const emit = defineEmits<{ logout: [] }>();
|
||||||
const openPopover = ref(false);
|
|
||||||
|
|
||||||
const { globalLockScreenShortcutKey, globalLogoutShortcutKey } =
|
const { globalLockScreenShortcutKey, globalLogoutShortcutKey } =
|
||||||
usePreferences();
|
usePreferences();
|
||||||
|
@ -84,6 +90,27 @@ const [LogoutModal, logoutModalApi] = useVbenModal({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const refTrigger = useTemplateRef('refTrigger');
|
||||||
|
const refContent = useTemplateRef('refContent');
|
||||||
|
const [openPopover, hoverWatcher] = useHoverToggle(
|
||||||
|
[refTrigger, refContent],
|
||||||
|
() => props.hoverDelay,
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.trigger === 'hover' || props.trigger === 'both',
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
hoverWatcher.enable();
|
||||||
|
} else {
|
||||||
|
hoverWatcher.disable();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
|
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
|
||||||
|
|
||||||
const enableLogoutShortcutKey = computed(() => {
|
const enableLogoutShortcutKey = computed(() => {
|
||||||
|
@ -155,8 +182,8 @@ if (enableShortcutKey.value) {
|
||||||
{{ $t('ui.widgets.logoutTip') }}
|
{{ $t('ui.widgets.logoutTip') }}
|
||||||
</LogoutModal>
|
</LogoutModal>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu v-model:open="openPopover">
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger ref="refTrigger" :disabled="props.trigger === 'hover'">
|
||||||
<div class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full p-1.5">
|
<div class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full p-1.5">
|
||||||
<div class="hover:text-accent-foreground flex-center">
|
<div class="hover:text-accent-foreground flex-center">
|
||||||
<VbenAvatar :alt="text" :src="avatar" class="size-8" dot />
|
<VbenAvatar :alt="text" :src="avatar" class="size-8" dot />
|
||||||
|
@ -164,64 +191,66 @@ if (enableShortcutKey.value) {
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent class="mr-2 min-w-[240px] p-0 pb-1">
|
<DropdownMenuContent class="mr-2 min-w-[240px] p-0 pb-1">
|
||||||
<DropdownMenuLabel class="flex items-center p-3">
|
<div ref="refContent">
|
||||||
<VbenAvatar
|
<DropdownMenuLabel class="flex items-center p-3">
|
||||||
:alt="text"
|
<VbenAvatar
|
||||||
:src="avatar"
|
:alt="text"
|
||||||
class="size-12"
|
:src="avatar"
|
||||||
dot
|
class="size-12"
|
||||||
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
|
dot
|
||||||
/>
|
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
|
||||||
<div class="ml-2 w-full">
|
/>
|
||||||
<div
|
<div class="ml-2 w-full">
|
||||||
v-if="tagText || text || $slots.tagText"
|
<div
|
||||||
class="text-foreground mb-1 flex items-center text-sm font-medium"
|
v-if="tagText || text || $slots.tagText"
|
||||||
>
|
class="text-foreground mb-1 flex items-center text-sm font-medium"
|
||||||
{{ text }}
|
>
|
||||||
<slot name="tagText">
|
{{ text }}
|
||||||
<Badge v-if="tagText" class="ml-2 text-green-400">
|
<slot name="tagText">
|
||||||
{{ tagText }}
|
<Badge v-if="tagText" class="ml-2 text-green-400">
|
||||||
</Badge>
|
{{ tagText }}
|
||||||
</slot>
|
</Badge>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground text-xs font-normal">
|
||||||
|
{{ description }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted-foreground text-xs font-normal">
|
</DropdownMenuLabel>
|
||||||
{{ description }}
|
<DropdownMenuSeparator v-if="menus?.length" />
|
||||||
</div>
|
<DropdownMenuItem
|
||||||
</div>
|
v-for="menu in menus"
|
||||||
</DropdownMenuLabel>
|
:key="menu.text"
|
||||||
<DropdownMenuSeparator v-if="menus?.length" />
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
<DropdownMenuItem
|
@click="menu.handler"
|
||||||
v-for="menu in menus"
|
>
|
||||||
:key="menu.text"
|
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
{{ menu.text }}
|
||||||
@click="menu.handler"
|
</DropdownMenuItem>
|
||||||
>
|
<DropdownMenuSeparator />
|
||||||
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
|
<DropdownMenuItem
|
||||||
{{ menu.text }}
|
v-if="preferences.widget.lockScreen"
|
||||||
</DropdownMenuItem>
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
<DropdownMenuSeparator />
|
@click="handleOpenLock"
|
||||||
<DropdownMenuItem
|
>
|
||||||
v-if="preferences.widget.lockScreen"
|
<LockKeyhole class="mr-2 size-4" />
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
{{ $t('ui.widgets.lockScreen.title') }}
|
||||||
@click="handleOpenLock"
|
<DropdownMenuShortcut v-if="enableLockScreenShortcutKey">
|
||||||
>
|
{{ altView }} L
|
||||||
<LockKeyhole class="mr-2 size-4" />
|
</DropdownMenuShortcut>
|
||||||
{{ $t('ui.widgets.lockScreen.title') }}
|
</DropdownMenuItem>
|
||||||
<DropdownMenuShortcut v-if="enableLockScreenShortcutKey">
|
<DropdownMenuSeparator v-if="preferences.widget.lockScreen" />
|
||||||
{{ altView }} L
|
<DropdownMenuItem
|
||||||
</DropdownMenuShortcut>
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
</DropdownMenuItem>
|
@click="handleLogout"
|
||||||
<DropdownMenuSeparator v-if="preferences.widget.lockScreen" />
|
>
|
||||||
<DropdownMenuItem
|
<LogOut class="mr-2 size-4" />
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
{{ $t('common.logout') }}
|
||||||
@click="handleLogout"
|
<DropdownMenuShortcut v-if="enableLogoutShortcutKey">
|
||||||
>
|
{{ altView }} Q
|
||||||
<LogOut class="mr-2 size-4" />
|
</DropdownMenuShortcut>
|
||||||
{{ $t('common.logout') }}
|
</DropdownMenuItem>
|
||||||
<DropdownMenuShortcut v-if="enableLogoutShortcutKey">
|
</div>
|
||||||
{{ altView }} Q
|
|
||||||
</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -132,6 +132,7 @@ watch(
|
||||||
:text="userStore.userInfo?.realName"
|
:text="userStore.userInfo?.realName"
|
||||||
description="ann.vben@gmail.com"
|
description="ann.vben@gmail.com"
|
||||||
tag-text="Pro"
|
tag-text="Pro"
|
||||||
|
trigger="both"
|
||||||
@logout="handleLogout"
|
@logout="handleLogout"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1548,6 +1548,9 @@ importers:
|
||||||
'@vben/utils':
|
'@vben/utils':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../utils
|
version: link:../../utils
|
||||||
|
'@vueuse/core':
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 12.0.0(typescript@5.7.2)
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.13
|
specifier: ^3.5.13
|
||||||
version: 3.5.13(typescript@5.7.2)
|
version: 3.5.13(typescript@5.7.2)
|
||||||
|
@ -10732,7 +10735,7 @@ snapshots:
|
||||||
'@babel/core': 7.26.0
|
'@babel/core': 7.26.0
|
||||||
'@babel/helper-compilation-targets': 7.25.9
|
'@babel/helper-compilation-targets': 7.25.9
|
||||||
'@babel/helper-plugin-utils': 7.25.9
|
'@babel/helper-plugin-utils': 7.25.9
|
||||||
debug: 4.3.7(supports-color@9.4.0)
|
debug: 4.4.0
|
||||||
lodash.debounce: 4.0.8
|
lodash.debounce: 4.0.8
|
||||||
resolve: 1.22.8
|
resolve: 1.22.8
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|
Loading…
Reference in New Issue