feat: 将锁定屏幕和退出登录图标从用户下拉菜单移至头部栏

将锁定屏幕(LockKeyhole)和退出登录(LogOut)图标按钮从 UserDropdown 下拉菜单
移至 LayoutHeader 头部栏,位于通知图标的右侧。

变更内容:
- header.vue: 新增 lock-screen-btn 和 logout-btn 插槽,位于 notification 之后;
  添加 LockScreenModal/LogoutModal 及快捷键(Alt+L/Alt+Q)支持;
  新增 avatar/text props 用于锁屏弹窗显示
- layout.vue: 透传 avatar/text props 和 logout 事件
- user-dropdown.vue: 移除锁定屏幕和退出登录的菜单项、弹窗及快捷键逻辑
- 所有 5 个应用 basic.vue: 向 BasicLayout 传递 avatar/text,绑定 @logout 事件

头部右侧图标顺序: ... → 全屏 → 通知🔔 → 锁定🔒 → 退出🚪 → 用户头像
This commit is contained in:
ddfabc
2026-06-06 23:24:54 +08:00
parent 748b182e44
commit 4459389d4a
8 changed files with 185 additions and 144 deletions

View File

@@ -217,7 +217,12 @@ watch(
</script>
<template>
<BasicLayout @clear-preferences-and-logout="handleLogout">
<BasicLayout
:avatar
:text="userStore.userInfo?.realName"
@clear-preferences-and-logout="handleLogout"
@logout="handleLogout"
>
<template #user-dropdown>
<UserDropdown
:avatar
@@ -225,7 +230,6 @@ watch(
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
@clear-preferences-and-logout="handleLogout"
/>
</template>

View File

@@ -217,7 +217,12 @@ watch(
</script>
<template>
<BasicLayout @clear-preferences-and-logout="handleLogout">
<BasicLayout
:avatar
:text="userStore.userInfo?.realName"
@clear-preferences-and-logout="handleLogout"
@logout="handleLogout"
>
<template #user-dropdown>
<UserDropdown
:avatar
@@ -225,7 +230,6 @@ watch(
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
@clear-preferences-and-logout="handleLogout"
/>
</template>

View File

@@ -217,7 +217,12 @@ watch(
</script>
<template>
<BasicLayout @clear-preferences-and-logout="handleLogout">
<BasicLayout
:avatar
:text="userStore.userInfo?.realName"
@clear-preferences-and-logout="handleLogout"
@logout="handleLogout"
>
<template #user-dropdown>
<UserDropdown
:avatar
@@ -225,7 +230,6 @@ watch(
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
@clear-preferences-and-logout="handleLogout"
/>
</template>

View File

@@ -217,7 +217,12 @@ watch(
</script>
<template>
<BasicLayout @clear-preferences-and-logout="handleLogout">
<BasicLayout
:avatar
:text="userStore.userInfo?.realName"
@clear-preferences-and-logout="handleLogout"
@logout="handleLogout"
>
<template #user-dropdown>
<UserDropdown
:avatar
@@ -225,7 +230,6 @@ watch(
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
@clear-preferences-and-logout="handleLogout"
/>
</template>

View File

@@ -217,7 +217,12 @@ watch(
</script>
<template>
<BasicLayout @clear-preferences-and-logout="handleLogout">
<BasicLayout
:avatar
:text="userStore.userInfo?.realName"
@clear-preferences-and-logout="handleLogout"
@logout="handleLogout"
>
<template #user-dropdown>
<UserDropdown
:avatar
@@ -225,7 +230,6 @@ watch(
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
@logout="handleLogout"
@clear-preferences-and-logout="handleLogout"
/>
</template>

View File

@@ -2,25 +2,38 @@
import { computed, useSlots } from 'vue';
import { useRefresh } from '@vben/hooks';
import { RotateCw } from '@vben/icons';
import { LockKeyhole, LogOut, RotateCw } from '@vben/icons';
import { $t } from '@vben/locales';
import { preferences, usePreferences } from '@vben/preferences';
import { useAccessStore } from '@vben/stores';
import { useVbenModal } from '@vben-core/popup-ui';
import { VbenFullScreen, VbenIconButton } from '@vben-core/shadcn-ui';
import { useMagicKeys, whenever } from '@vueuse/core';
import {
GlobalSearch,
LanguageToggle,
LockScreenModal,
PreferencesButton,
ThemeToggle,
TimezoneButton,
} from '../../widgets';
interface Props {
/**
* 头像
*/
avatar?: string;
/**
* Logo 主题
*/
theme?: string;
/**
* 用户文本
*/
text?: string;
}
defineOptions({
@@ -28,18 +41,79 @@ defineOptions({
});
withDefaults(defineProps<Props>(), {
avatar: '',
theme: 'light',
text: '',
});
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
const emit = defineEmits<{
clearPreferencesAndLogout: [];
logout: [];
openLockScreen: [];
}>();
const REFERENCE_VALUE = 100;
const accessStore = useAccessStore();
const { globalSearchShortcutKey, preferencesButtonPosition } = usePreferences();
const {
globalLockScreenShortcutKey,
globalLogoutShortcutKey,
globalSearchShortcutKey,
preferencesButtonPosition,
} = usePreferences();
const slots = useSlots();
const { refresh } = useRefresh();
const [LockModal, lockModalApi] = useVbenModal({
connectedComponent: LockScreenModal,
});
const [LogoutModal, logoutModalApi] = useVbenModal({
onConfirm() {
handleSubmitLogout();
},
});
function handleOpenLock() {
lockModalApi.open();
}
function handleSubmitLock(lockScreenPassword: string) {
lockModalApi.close();
accessStore.lockScreen(lockScreenPassword);
}
function handleLogout() {
logoutModalApi.open();
}
function handleSubmitLogout() {
emit('logout');
logoutModalApi.close();
}
// 快捷键
if (preferences.shortcutKeys.enable) {
const keys = useMagicKeys();
const lockKey = keys['Alt+KeyL'];
const logoutKey = keys['Alt+KeyQ'];
if (lockKey) {
whenever(lockKey, () => {
if (globalLockScreenShortcutKey?.value) {
handleOpenLock();
}
});
}
if (logoutKey) {
whenever(logoutKey, () => {
if (globalLogoutShortcutKey?.value) {
handleLogout();
}
});
}
}
/**
* 插槽列表类型
*/
@@ -94,6 +168,18 @@ const rightSlots = computed(() => {
name: 'notification',
});
}
// 锁定屏幕
if (preferences.widget.lockScreen) {
list.push({
index: REFERENCE_VALUE + 70,
name: 'lock-screen-btn',
});
}
// 退出登录
list.push({
index: REFERENCE_VALUE + 80,
name: 'logout-btn',
});
Object.keys(slots).forEach((key) => {
// 适配插槽名称例如第一个插槽名header-right-1
@@ -150,6 +236,26 @@ function clearPreferencesAndLogout() {
</script>
<template>
<LockModal
v-if="preferences.widget.lockScreen"
:avatar="avatar"
:text="text"
@submit="handleSubmitLock"
/>
<LogoutModal
: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>
<template
v-for="slot in leftSlots.filter((item) => item.index < REFERENCE_VALUE)"
:key="slot.name"
@@ -206,6 +312,24 @@ function clearPreferencesAndLogout() {
<template v-else-if="slot.name === 'timezone'">
<TimezoneButton class="mt-0.5 mr-1" />
</template>
<template v-else-if="slot.name === 'lock-screen-btn'">
<VbenIconButton
class="mr-1"
:tooltip="$t('ui.widgets.lockScreen.title')"
@click="handleOpenLock"
>
<LockKeyhole class="size-4" />
</VbenIconButton>
</template>
<template v-else-if="slot.name === 'logout-btn'">
<VbenIconButton
class="mr-1"
:tooltip="$t('common.logout')"
@click="handleLogout"
>
<LogOut class="size-4" />
</VbenIconButton>
</template>
</slot>
</template>
</div>

View File

@@ -36,7 +36,21 @@ import { LayoutTabbar } from './tabbar';
defineOptions({ name: 'BasicLayout' });
const emit = defineEmits<{ clearPreferencesAndLogout: []; clickLogo: [] }>();
withDefaults(defineProps<Props>(), {
avatar: '',
text: '',
});
const emit = defineEmits<{
clearPreferencesAndLogout: [];
clickLogo: [];
logout: [];
}>();
interface Props {
avatar?: string;
text?: string;
}
const {
isDark,
@@ -167,6 +181,10 @@ function clearPreferencesAndLogout() {
emit('clearPreferencesAndLogout');
}
function handleLogout() {
emit('logout');
}
function clickLogo() {
emit('clickLogo');
}
@@ -303,8 +321,11 @@ const headerSlots = computed(() => {
<!-- 头部区域 -->
<template #header>
<LayoutHeader
:avatar="avatar"
:theme="theme"
:text="text"
@clear-preferences-and-logout="clearPreferencesAndLogout"
@logout="handleLogout"
>
<template
v-if="!showHeaderNav && preferences.breadcrumb.enable"

View File

@@ -3,16 +3,13 @@ import type { Component } from 'vue';
import type { AnyFunction } from '@vben/types';
import { computed, useTemplateRef, watch } from 'vue';
import { useTemplateRef, watch } from 'vue';
import { useHoverToggle } from '@vben/hooks';
import { LockKeyhole, LogOut, Settings } from '@vben/icons';
import { Settings } 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 { usePreferences } from '@vben/preferences';
import { useVbenModal } from '@vben-core/popup-ui';
import {
Badge,
DropdownMenu,
@@ -20,15 +17,11 @@ import {
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 {
@@ -40,10 +33,6 @@ interface Props {
* @zh_CN 描述
*/
description?: string;
/**
* 是否启用快捷键
*/
enableShortcutKey?: boolean;
/**
* 菜单数组
*/
@@ -74,31 +63,16 @@ defineOptions({
const props = withDefaults(defineProps<Props>(), {
avatar: '',
description: '',
enableShortcutKey: true,
menus: () => [],
showShortcutKey: true,
tagText: '',
text: '',
trigger: 'click',
hoverDelay: 500,
});
const emit = defineEmits<{ clearPreferencesAndLogout: []; logout: [] }>();
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
const {
globalLockScreenShortcutKey,
globalLogoutShortcutKey,
preferencesButtonPosition,
} = usePreferences();
const accessStore = useAccessStore();
const [LockModal, lockModalApi] = useVbenModal({
connectedComponent: LockScreenModal,
});
const [LogoutModal, logoutModalApi] = useVbenModal({
onConfirm() {
handleSubmitLogout();
},
});
const { preferencesButtonPosition } = usePreferences();
const refTrigger = useTemplateRef('refTrigger');
const refContent = useTemplateRef('refContent');
@@ -122,89 +96,13 @@ watch(
},
);
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
const enableLogoutShortcutKey = computed(() => {
return props.enableShortcutKey && globalLogoutShortcutKey.value;
});
const enableLockScreenShortcutKey = computed(() => {
return props.enableShortcutKey && globalLockScreenShortcutKey.value;
});
const enableShortcutKey = computed(() => {
return props.enableShortcutKey && preferences.shortcutKeys.enable;
});
function handleOpenLock() {
lockModalApi.open();
}
function handleSubmitLock(lockScreenPassword: string) {
lockModalApi.close();
accessStore.lockScreen(lockScreenPassword);
}
function handleLogout() {
// emit
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="preferences.widget.lockScreen"
:avatar="avatar"
:text="text"
@submit="handleSubmitLock"
/>
<LogoutModal
: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"
@@ -257,7 +155,7 @@ if (enableShortcutKey.value) {
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
{{ menu.text }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSeparator v-if="preferencesButtonPosition.userDropdown" />
<DropdownMenuItem
v-if="preferencesButtonPosition.userDropdown"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@@ -266,28 +164,6 @@ if (enableShortcutKey.value) {
<Settings class="mr-2 size-4" />
{{ $t('preferences.title') }}
</DropdownMenuItem>
<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>