Files
xk-admin/packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue
金毛88 0cd87c170f feat: 顶栏按钮支持位置选择(header/user-dropdown/none)
为顶栏所有部件按钮添加独立的位置配置,支持在顶栏、用户下拉菜单、不显示
三种选项间切换,每个按钮可独立设置。

- 新增 7 个位置偏好字段:globalSearchButtonPosition、
  themeToggleButtonPosition、languageToggleButtonPosition、
  fullscreenButtonPosition、notificationButtonPosition、
  refreshButtonPosition、timezoneButtonPosition
- 合并 lockScreenButtonPosition、logoutButtonPosition
- header.vue 按位置条件过滤按钮显示
- user-dropdown.vue 新增对应菜单项,支持在用户下拉菜单中显示
- 偏好设置界面仅保留位置选择器(不再需要独立的开关)
- 更新中英文国际化文案

Co-Authored-By: AtomCode (deepseek-v4-flash) <noreply@atomgit.com>
2026-07-18 22:46:31 +08:00

440 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type { Component } from 'vue';
import type { AnyFunction } from '@vben/types';
import { computed, useTemplateRef, watch } from 'vue';
import { useHoverToggle } from '@vben/hooks';
import {
Bell,
Fullscreen,
Languages,
LockKeyhole,
LogOut,
RotateCw,
Search,
Settings,
Sun,
SunMoon,
} from '@vben/icons';
import { $t } from '@vben/locales';
import { preferences, usePreferences } from '@vben/preferences';
import { useAccessStore } from '@vben/stores';
import { isWindowsOs } from '@vben/utils';
import { useVbenModal } from '@vben-core/popup-ui';
import {
Badge,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
VbenAvatar,
VbenIcon,
} from '@vben-core/shadcn-ui';
import { useMagicKeys, whenever } from '@vueuse/core';
import { LockScreenModal } from '../lock-screen';
import { Preferences } from '../preferences';
interface Props {
/**
* 头像
*/
avatar?: string;
/**
* @zh_CN 描述
*/
description?: string;
/**
* 菜单数组
*/
menus?: Array<{
handler: AnyFunction;
icon?: Component | Function | string;
text: string;
}>;
/**
* 标签文本
*/
tagText?: string;
/**
* 文本
*/
text?: string;
/** 触发方式 */
trigger?: 'both' | 'click' | 'hover';
/** hover触发时延迟响应的时间 */
hoverDelay?: number;
}
defineOptions({
name: 'UserDropdown',
});
const props = withDefaults(defineProps<Props>(), {
avatar: '',
description: '',
menus: () => [],
tagText: '',
text: '',
trigger: 'click',
hoverDelay: 500,
});
const emit = defineEmits<{ clearPreferencesAndLogout: []; logout: [] }>();
const {
globalLockScreenShortcutKey,
globalLogoutShortcutKey,
preferencesButtonPosition,
} = usePreferences();
const accessStore = useAccessStore();
const [LockModal, lockModalApi] = useVbenModal({
connectedComponent: LockScreenModal,
});
const [LogoutModal, logoutModalApi] = useVbenModal({
onConfirm() {
handleSubmitLogout();
},
});
const refTrigger = useTemplateRef('refTrigger');
const refContent = useTemplateRef('refContent');
const refPreferences = useTemplateRef('refPreferences');
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 showLockInDropdown = computed(
() =>
preferences.widget.lockScreen &&
preferences.widget.lockScreenButtonPosition === 'user-dropdown',
);
const showLogoutInDropdown = computed(
() => preferences.widget.logoutButtonPosition === 'user-dropdown',
);
const showGlobalSearchInDropdown = computed(
() =>
preferences.widget.globalSearch &&
preferences.widget.globalSearchButtonPosition === 'user-dropdown',
);
const showThemeToggleInDropdown = computed(
() =>
preferences.widget.themeToggle &&
preferences.widget.themeToggleButtonPosition === 'user-dropdown',
);
const showLanguageToggleInDropdown = computed(
() =>
preferences.widget.languageToggle &&
preferences.widget.languageToggleButtonPosition === 'user-dropdown',
);
const showTimezoneInDropdown = computed(
() =>
preferences.widget.timezone &&
preferences.widget.timezoneButtonPosition === 'user-dropdown',
);
const showFullscreenInDropdown = computed(
() =>
preferences.widget.fullscreen &&
preferences.widget.fullscreenButtonPosition === 'user-dropdown',
);
const showNotificationInDropdown = computed(
() =>
preferences.widget.notification &&
preferences.widget.notificationButtonPosition === 'user-dropdown',
);
const showRefreshInDropdown = computed(
() =>
preferences.widget.refresh &&
preferences.widget.refreshButtonPosition === 'user-dropdown',
);
const hasAnyInDropdown = computed(
() =>
showLockInDropdown.value ||
showLogoutInDropdown.value ||
showGlobalSearchInDropdown.value ||
showThemeToggleInDropdown.value ||
showLanguageToggleInDropdown.value ||
showTimezoneInDropdown.value ||
showFullscreenInDropdown.value ||
showNotificationInDropdown.value ||
showRefreshInDropdown.value,
);
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
const enableLogoutShortcutKey = computed(() => {
return showLogoutInDropdown.value && globalLogoutShortcutKey.value;
});
const enableLockScreenShortcutKey = computed(() => {
return showLockInDropdown.value && globalLockScreenShortcutKey.value;
});
const enableShortcutKey = computed(() => {
return (
(showLockInDropdown.value || showLogoutInDropdown.value) &&
preferences.shortcutKeys.enable
);
});
function handleOpenLock() {
lockModalApi.open();
}
function handleSubmitLock(lockScreenPassword: string) {
lockModalApi.close();
accessStore.lockScreen(lockScreenPassword);
}
function handleLogout() {
logoutModalApi.open();
openPopover.value = false;
}
function handleSubmitLogout() {
emit('logout');
logoutModalApi.close();
}
// 设置 - 打开偏好设置抽屉
function handleOpenSettings() {
refPreferences.value?.open();
}
if (enableShortcutKey.value) {
const keys = useMagicKeys();
const logoutKey = keys['Alt+KeyQ'];
const lockKey = keys['Alt+KeyL'];
if (logoutKey) {
whenever(logoutKey, () => {
if (enableLogoutShortcutKey.value) {
handleLogout();
}
});
}
if (lockKey) {
whenever(lockKey, () => {
if (enableLockScreenShortcutKey.value) {
handleOpenLock();
}
});
}
}
</script>
<template>
<LockModal
v-if="showLockInDropdown"
:avatar="avatar"
:text="text"
@submit="handleSubmitLock"
/>
<LogoutModal
v-if="showLogoutInDropdown"
:cancel-text="$t('common.cancel')"
:confirm-text="$t('common.confirm')"
:fullscreen-button="false"
:title="$t('common.prompt')"
centered
content-class="px-8 min-h-10"
footer-class="border-none mb-3 mr-3"
header-class="border-none"
>
{{ $t('ui.widgets.logoutTip') }}
</LogoutModal>
<Preferences
v-if="preferencesButtonPosition.userDropdown"
ref="refPreferences"
:show-button="false"
@clear-preferences-and-logout="emit('clearPreferencesAndLogout')"
/>
<DropdownMenu v-model:open="openPopover" :modal="false">
<DropdownMenuTrigger ref="refTrigger" :disabled="props.trigger === 'hover'">
<div class="mr-2 ml-1 cursor-pointer rounded-full p-1.5 hover:bg-accent">
<div class="flex-center hover:text-accent-foreground">
<VbenAvatar :alt="text" :src="avatar" class="size-8" dot />
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent class="mr-2 min-w-60 p-0 pb-1">
<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="mb-1 flex items-center text-sm font-medium text-foreground"
>
{{ text }}
<slot name="tagText">
<Badge
v-if="tagText"
variant="secondary"
class="ml-2 text-green-400"
>
{{ tagText }}
</Badge>
</slot>
</div>
<div class="text-xs font-normal text-muted-foreground">
{{ 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>
<template v-if="showLockInDropdown || showLogoutInDropdown">
<DropdownMenuSeparator v-if="showLockInDropdown" />
<DropdownMenuItem
v-if="showLockInDropdown"
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="showLogoutInDropdown" />
<DropdownMenuItem
v-if="showLogoutInDropdown"
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>
</template>
<template
v-if="
showGlobalSearchInDropdown ||
showThemeToggleInDropdown ||
showLanguageToggleInDropdown ||
showTimezoneInDropdown ||
showFullscreenInDropdown ||
showNotificationInDropdown ||
showRefreshInDropdown
"
>
<DropdownMenuSeparator />
<DropdownMenuItem
v-if="showGlobalSearchInDropdown"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
>
<Search class="mr-2 size-4" />
{{ $t('preferences.widget.globalSearch') }}
</DropdownMenuItem>
<DropdownMenuItem
v-if="showThemeToggleInDropdown"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
>
<Sun class="mr-2 size-4" />
{{ $t('preferences.widget.themeToggle') }}
</DropdownMenuItem>
<DropdownMenuItem
v-if="showLanguageToggleInDropdown"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
>
<Languages class="mr-2 size-4" />
{{ $t('preferences.widget.languageToggle') }}
</DropdownMenuItem>
<DropdownMenuItem
v-if="showTimezoneInDropdown"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
>
<SunMoon class="mr-2 size-4" />
{{ $t('preferences.widget.timezone') }}
</DropdownMenuItem>
<DropdownMenuItem
v-if="showFullscreenInDropdown"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
>
<Fullscreen class="mr-2 size-4" />
{{ $t('preferences.widget.fullscreen') }}
</DropdownMenuItem>
<DropdownMenuItem
v-if="showNotificationInDropdown"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
>
<Bell class="mr-2 size-4" />
{{ $t('preferences.widget.notification') }}
</DropdownMenuItem>
<DropdownMenuItem
v-if="showRefreshInDropdown"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
>
<RotateCw class="mr-2 size-4" />
{{ $t('preferences.widget.refresh') }}
</DropdownMenuItem>
</template>
<DropdownMenuSeparator
v-if="hasAnyInDropdown || preferencesButtonPosition.userDropdown"
/>
<DropdownMenuItem
v-if="preferencesButtonPosition.userDropdown"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleOpenSettings"
>
<Settings class="mr-2 size-4" />
{{ $t('preferences.title') }}
</DropdownMenuItem>
</div>
</DropdownMenuContent>
</DropdownMenu>
</template>