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/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:"

View File

@ -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';

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 { 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>

View File

@ -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>

View File

@ -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: