feat: user-dropdown support `hover` trigger (#5143)

* feat: user-dropdown support `hover` trigger

* fix: modified type declaration
dev-v5
Netfan 2024-12-15 18:24:22 +08:00 committed by GitHub
parent 7581fb381f
commit f446cbf9e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 160 additions and 62 deletions

View File

@ -25,6 +25,7 @@
"@vben/stores": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:",
"watermark-js-plus": "catalog:"

View File

@ -1,6 +1,7 @@
export * from './use-app-config';
export * from './use-content-maximize';
export * from './use-design-tokens';
export * from './use-hover-toggle';
export * from './use-pagination';
export * from './use-refresh';
export * from './use-tabs';

View File

@ -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];
}

View File

@ -2,8 +2,9 @@
import type { AnyFunction } from '@vben/types';
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 { $t } from '@vben/locales';
import { preferences, usePreferences } from '@vben/preferences';
@ -53,6 +54,10 @@ interface Props {
* 文本
*/
text?: string;
/** 触发方式 */
trigger?: 'both' | 'click' | 'hover';
/** hover触发时延迟响应的时间 */
hoverDelay?: number;
}
defineOptions({
@ -67,10 +72,11 @@ const props = withDefaults(defineProps<Props>(), {
showShortcutKey: true,
tagText: '',
text: '',
trigger: 'click',
hoverDelay: 500,
});
const emit = defineEmits<{ logout: [] }>();
const openPopover = ref(false);
const { globalLockScreenShortcutKey, globalLogoutShortcutKey } =
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 enableLogoutShortcutKey = computed(() => {
@ -155,8 +182,8 @@ if (enableShortcutKey.value) {
{{ $t('ui.widgets.logoutTip') }}
</LogoutModal>
<DropdownMenu>
<DropdownMenuTrigger>
<DropdownMenu v-model:open="openPopover">
<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:text-accent-foreground flex-center">
<VbenAvatar :alt="text" :src="avatar" class="size-8" dot />
@ -164,64 +191,66 @@ if (enableShortcutKey.value) {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent class="mr-2 min-w-[240px] p-0 pb-1">
<DropdownMenuLabel class="flex items-center p-3">
<VbenAvatar
:alt="text"
:src="avatar"
class="size-12"
dot
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
/>
<div class="ml-2 w-full">
<div
v-if="tagText || text || $slots.tagText"
class="text-foreground mb-1 flex items-center text-sm font-medium"
>
{{ text }}
<slot name="tagText">
<Badge v-if="tagText" class="ml-2 text-green-400">
{{ tagText }}
</Badge>
</slot>
<div ref="refContent">
<DropdownMenuLabel class="flex items-center p-3">
<VbenAvatar
:alt="text"
:src="avatar"
class="size-12"
dot
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
/>
<div class="ml-2 w-full">
<div
v-if="tagText || text || $slots.tagText"
class="text-foreground mb-1 flex items-center text-sm font-medium"
>
{{ text }}
<slot name="tagText">
<Badge v-if="tagText" class="ml-2 text-green-400">
{{ tagText }}
</Badge>
</slot>
</div>
<div class="text-muted-foreground text-xs font-normal">
{{ description }}
</div>
</div>
<div class="text-muted-foreground text-xs font-normal">
{{ description }}
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator v-if="menus?.length" />
<DropdownMenuItem
v-for="menu in menus"
:key="menu.text"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="menu.handler"
>
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
{{ menu.text }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
v-if="preferences.widget.lockScreen"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleOpenLock"
>
<LockKeyhole class="mr-2 size-4" />
{{ $t('ui.widgets.lockScreen.title') }}
<DropdownMenuShortcut v-if="enableLockScreenShortcutKey">
{{ altView }} L
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator v-if="preferences.widget.lockScreen" />
<DropdownMenuItem
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleLogout"
>
<LogOut class="mr-2 size-4" />
{{ $t('common.logout') }}
<DropdownMenuShortcut v-if="enableLogoutShortcutKey">
{{ altView }} Q
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuLabel>
<DropdownMenuSeparator v-if="menus?.length" />
<DropdownMenuItem
v-for="menu in menus"
:key="menu.text"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="menu.handler"
>
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
{{ menu.text }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
v-if="preferences.widget.lockScreen"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleOpenLock"
>
<LockKeyhole class="mr-2 size-4" />
{{ $t('ui.widgets.lockScreen.title') }}
<DropdownMenuShortcut v-if="enableLockScreenShortcutKey">
{{ altView }} L
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator v-if="preferences.widget.lockScreen" />
<DropdownMenuItem
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleLogout"
>
<LogOut class="mr-2 size-4" />
{{ $t('common.logout') }}
<DropdownMenuShortcut v-if="enableLogoutShortcutKey">
{{ altView }} Q
</DropdownMenuShortcut>
</DropdownMenuItem>
</div>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@ -132,6 +132,7 @@ watch(
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
trigger="both"
@logout="handleLogout"
/>
</template>

View File

@ -1548,6 +1548,9 @@ importers:
'@vben/utils':
specifier: workspace:*
version: link:../../utils
'@vueuse/core':
specifier: 'catalog:'
version: 12.0.0(typescript@5.7.2)
vue:
specifier: ^3.5.13
version: 3.5.13(typescript@5.7.2)
@ -10732,7 +10735,7 @@ snapshots:
'@babel/core': 7.26.0
'@babel/helper-compilation-targets': 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
resolve: 1.22.8
transitivePeerDependencies: