diff --git a/apps/web-antd/src/layouts/basic.vue b/apps/web-antd/src/layouts/basic.vue
index 3b74354c..e85dd1cf 100644
--- a/apps/web-antd/src/layouts/basic.vue
+++ b/apps/web-antd/src/layouts/basic.vue
@@ -231,6 +231,7 @@ watch(
description="ann.vben@gmail.com"
tag-text="Pro"
@clear-preferences-and-logout="handleLogout"
+ @logout="handleLogout"
/>
diff --git a/apps/web-antdv-next/src/layouts/basic.vue b/apps/web-antdv-next/src/layouts/basic.vue
index 3b74354c..e85dd1cf 100644
--- a/apps/web-antdv-next/src/layouts/basic.vue
+++ b/apps/web-antdv-next/src/layouts/basic.vue
@@ -231,6 +231,7 @@ watch(
description="ann.vben@gmail.com"
tag-text="Pro"
@clear-preferences-and-logout="handleLogout"
+ @logout="handleLogout"
/>
diff --git a/apps/web-ele/src/layouts/basic.vue b/apps/web-ele/src/layouts/basic.vue
index 3b74354c..e85dd1cf 100644
--- a/apps/web-ele/src/layouts/basic.vue
+++ b/apps/web-ele/src/layouts/basic.vue
@@ -231,6 +231,7 @@ watch(
description="ann.vben@gmail.com"
tag-text="Pro"
@clear-preferences-and-logout="handleLogout"
+ @logout="handleLogout"
/>
diff --git a/apps/web-naive/src/layouts/basic.vue b/apps/web-naive/src/layouts/basic.vue
index 3b74354c..e85dd1cf 100644
--- a/apps/web-naive/src/layouts/basic.vue
+++ b/apps/web-naive/src/layouts/basic.vue
@@ -231,6 +231,7 @@ watch(
description="ann.vben@gmail.com"
tag-text="Pro"
@clear-preferences-and-logout="handleLogout"
+ @logout="handleLogout"
/>
diff --git a/apps/web-tdesign/src/layouts/basic.vue b/apps/web-tdesign/src/layouts/basic.vue
index 3b74354c..e85dd1cf 100644
--- a/apps/web-tdesign/src/layouts/basic.vue
+++ b/apps/web-tdesign/src/layouts/basic.vue
@@ -231,6 +231,7 @@ watch(
description="ann.vben@gmail.com"
tag-text="Pro"
@clear-preferences-and-logout="handleLogout"
+ @logout="handleLogout"
/>
diff --git a/packages/@core/preferences/src/config.ts b/packages/@core/preferences/src/config.ts
index 9160a870..edf97c1f 100644
--- a/packages/@core/preferences/src/config.ts
+++ b/packages/@core/preferences/src/config.ts
@@ -137,14 +137,23 @@ const defaultPreferences: Preferences = {
},
widget: {
fullscreen: true,
+ fullscreenButtonPosition: 'header',
globalSearch: true,
+ globalSearchButtonPosition: 'header',
languageToggle: true,
+ languageToggleButtonPosition: 'header',
lockScreen: true,
+ lockScreenButtonPosition: 'header',
+ logoutButtonPosition: 'header',
notification: true,
+ notificationButtonPosition: 'header',
refresh: true,
+ refreshButtonPosition: 'header',
sidebarToggle: true,
themeToggle: true,
+ themeToggleButtonPosition: 'header',
timezone: true,
+ timezoneButtonPosition: 'header',
},
};
diff --git a/packages/@core/preferences/src/types.ts b/packages/@core/preferences/src/types.ts
index 5834c34c..9cd39e20 100644
--- a/packages/@core/preferences/src/types.ts
+++ b/packages/@core/preferences/src/types.ts
@@ -365,22 +365,40 @@ interface TransitionPreferences {
interface WidgetPreferences {
/** 是否启用全屏部件 */
fullscreen: boolean;
+ /** 全屏按钮位置 */
+ fullscreenButtonPosition: 'header' | 'none' | 'user-dropdown';
/** 是否启用全局搜索部件 */
globalSearch: boolean;
+ /** 全局搜索按钮位置 */
+ globalSearchButtonPosition: 'header' | 'none' | 'user-dropdown';
/** 是否启用语言切换部件 */
languageToggle: boolean;
+ /** 语言切换按钮位置 */
+ languageToggleButtonPosition: 'header' | 'none' | 'user-dropdown';
/** 是否开启锁屏功能 */
lockScreen: boolean;
+ /** 锁屏按钮位置 */
+ lockScreenButtonPosition: 'header' | 'none' | 'user-dropdown';
+ /** 退出登录按钮位置 */
+ logoutButtonPosition: 'header' | 'none' | 'user-dropdown';
/** 是否显示通知部件 */
notification: boolean;
+ /** 通知按钮位置 */
+ notificationButtonPosition: 'header' | 'none' | 'user-dropdown';
/** 显示刷新按钮 */
refresh: boolean;
+ /** 刷新按钮位置 */
+ refreshButtonPosition: 'header' | 'none' | 'user-dropdown';
/** 是否显示侧边栏显示/隐藏部件 */
sidebarToggle: boolean;
/** 是否显示主题切换部件 */
themeToggle: boolean;
+ /** 主题切换按钮位置 */
+ themeToggleButtonPosition: 'header' | 'none' | 'user-dropdown';
/** 是否显示时区部件 */
timezone: boolean;
+ /** 时区按钮位置 */
+ timezoneButtonPosition: 'header' | 'none' | 'user-dropdown';
}
interface Preferences {
diff --git a/packages/effects/layouts/src/basic/header/header.vue b/packages/effects/layouts/src/basic/header/header.vue
index 8b727d51..4aa77f70 100644
--- a/packages/effects/layouts/src/basic/header/header.vue
+++ b/packages/effects/layouts/src/basic/header/header.vue
@@ -122,7 +122,10 @@ type SlotItem = { index: number; name: string };
const rightSlots = computed(() => {
const list: Array = [];
// 全局搜索
- if (preferences.widget.globalSearch) {
+ if (
+ preferences.widget.globalSearch &&
+ preferences.widget.globalSearchButtonPosition === 'header'
+ ) {
list.push({
index: REFERENCE_VALUE,
name: 'global-search',
@@ -137,50 +140,70 @@ const rightSlots = computed(() => {
}
// 主题、语言、时区等子功能由各自的 widget 开关独立控制,
// 不应跟随偏好设置按钮的显示与否(如 enablePreferences 为 false 或按钮位于其他位置时)
- if (preferences.widget.themeToggle) {
+ if (
+ preferences.widget.themeToggle &&
+ preferences.widget.themeToggleButtonPosition === 'header'
+ ) {
list.push({
index: REFERENCE_VALUE + 20,
name: 'theme-toggle',
});
}
- if (preferences.widget.languageToggle) {
+ if (
+ preferences.widget.languageToggle &&
+ preferences.widget.languageToggleButtonPosition === 'header'
+ ) {
list.push({
index: REFERENCE_VALUE + 30,
name: 'language-toggle',
});
}
- if (preferences.widget.timezone) {
+ if (
+ preferences.widget.timezone &&
+ preferences.widget.timezoneButtonPosition === 'header'
+ ) {
list.push({
index: REFERENCE_VALUE + 40,
name: 'timezone',
});
}
// 全屏
- if (preferences.widget.fullscreen) {
+ if (
+ preferences.widget.fullscreen &&
+ preferences.widget.fullscreenButtonPosition === 'header'
+ ) {
list.push({
index: REFERENCE_VALUE + 50,
name: 'fullscreen',
});
}
// 消息通知
- if (preferences.widget.notification) {
+ if (
+ preferences.widget.notification &&
+ preferences.widget.notificationButtonPosition === 'header'
+ ) {
list.push({
index: REFERENCE_VALUE + 60,
name: 'notification',
});
}
- // 锁定屏幕
- if (preferences.widget.lockScreen) {
+ // 锁定屏幕(仅在 header 位置显示)
+ if (
+ preferences.widget.lockScreen &&
+ preferences.widget.lockScreenButtonPosition === 'header'
+ ) {
list.push({
index: REFERENCE_VALUE + 70,
name: 'lock-screen-btn',
});
}
- // 退出登录
- list.push({
- index: REFERENCE_VALUE + 80,
- name: 'logout-btn',
- });
+ // 退出登录(仅在 header 位置显示)
+ if (preferences.widget.logoutButtonPosition === 'header') {
+ list.push({
+ index: REFERENCE_VALUE + 80,
+ name: 'logout-btn',
+ });
+ }
Object.keys(slots).forEach((key) => {
// 适配插槽名称,例如第一个插槽名:header-right-1
@@ -201,7 +224,10 @@ const rightSlots = computed(() => {
const leftSlots = computed(() => {
const list: Array = [];
// 刷新
- if (preferences.widget.refresh) {
+ if (
+ preferences.widget.refresh &&
+ preferences.widget.refreshButtonPosition === 'header'
+ ) {
list.push({
index: 0,
name: 'refresh',
diff --git a/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue b/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue
index 39cd14e5..07eb86be 100644
--- a/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue
+++ b/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue
@@ -6,24 +6,41 @@ import { computed } from 'vue';
import { $t } from '@vben/locales';
import SelectItem from '../select-item.vue';
-import SwitchItem from '../switch-item.vue';
defineOptions({
name: 'PreferenceInterfaceControl',
});
-const widgetGlobalSearch = defineModel('widgetGlobalSearch');
-const widgetFullscreen = defineModel('widgetFullscreen');
-const widgetLanguageToggle = defineModel('widgetLanguageToggle');
-const widgetNotification = defineModel('widgetNotification');
-const widgetThemeToggle = defineModel('widgetThemeToggle');
-const widgetSidebarToggle = defineModel('widgetSidebarToggle');
-const widgetLockScreen = defineModel('widgetLockScreen');
+const widgetGlobalSearchButtonPosition = defineModel(
+ 'widgetGlobalSearchButtonPosition',
+);
+const widgetFullscreenButtonPosition = defineModel(
+ 'widgetFullscreenButtonPosition',
+);
+const widgetLanguageToggleButtonPosition = defineModel(
+ 'widgetLanguageToggleButtonPosition',
+);
+const widgetNotificationButtonPosition = defineModel(
+ 'widgetNotificationButtonPosition',
+);
+const widgetThemeToggleButtonPosition = defineModel(
+ 'widgetThemeToggleButtonPosition',
+);
+const widgetLockScreenButtonPosition = defineModel(
+ 'widgetLockScreenButtonPosition',
+);
+const widgetLogoutButtonPosition = defineModel(
+ 'widgetLogoutButtonPosition',
+);
const appPreferencesButtonPosition = defineModel(
'appPreferencesButtonPosition',
);
-const widgetRefresh = defineModel('widgetRefresh');
-const widgetTimezone = defineModel('widgetTimezone');
+const widgetRefreshButtonPosition = defineModel(
+ 'widgetRefreshButtonPosition',
+);
+const widgetTimezoneButtonPosition = defineModel(
+ 'widgetTimezoneButtonPosition',
+);
const positionItems = computed((): SelectOption[] => [
{
@@ -43,36 +60,75 @@ const positionItems = computed((): SelectOption[] => [
value: 'user-dropdown',
},
]);
+
+const buttonPositionItems = computed((): SelectOption[] => [
+ {
+ label: $t('preferences.widget.header'),
+ value: 'header',
+ },
+ {
+ label: $t('preferences.widget.userDropdown'),
+ value: 'user-dropdown',
+ },
+ {
+ label: $t('common.notShow'),
+ value: 'none',
+ },
+]);
-
- {{ $t('preferences.widget.globalSearch') }}
-
-
- {{ $t('preferences.widget.themeToggle') }}
-
-
- {{ $t('preferences.widget.languageToggle') }}
-
-
- {{ $t('preferences.widget.fullscreen') }}
-
-
- {{ $t('preferences.widget.notification') }}
-
-
- {{ $t('preferences.widget.lockScreen') }}
-
-
- {{ $t('preferences.widget.sidebarToggle') }}
-
-
- {{ $t('preferences.widget.refresh') }}
-
-
- {{ $t('preferences.widget.timezone') }}
-
+
+ {{ $t('preferences.widget.globalSearchPosition') }}
+
+
+ {{ $t('preferences.widget.themeTogglePosition') }}
+
+
+ {{ $t('preferences.widget.languageTogglePosition') }}
+
+
+ {{ $t('preferences.widget.fullscreenPosition') }}
+
+
+ {{ $t('preferences.widget.notificationPosition') }}
+
+
+ {{ $t('preferences.widget.lockScreenPosition') }}
+
+
+ {{ $t('preferences.widget.logoutButtonPosition') }}
+
+
+ {{ $t('preferences.widget.refreshPosition') }}
+
+
+ {{ $t('preferences.widget.timezonePosition') }}
+
{{ $t('preferences.position.title') }}
diff --git a/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue b/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue
index 66e09a71..3e3ebdf9 100644
--- a/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue
+++ b/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue
@@ -174,15 +174,33 @@ const shortcutKeysGlobalLockScreen = defineModel(
'shortcutKeysGlobalLockScreen',
);
-const widgetGlobalSearch = defineModel('widgetGlobalSearch');
-const widgetFullscreen = defineModel('widgetFullscreen');
-const widgetLanguageToggle = defineModel('widgetLanguageToggle');
-const widgetNotification = defineModel('widgetNotification');
-const widgetThemeToggle = defineModel('widgetThemeToggle');
-const widgetSidebarToggle = defineModel('widgetSidebarToggle');
-const widgetLockScreen = defineModel('widgetLockScreen');
-const widgetRefresh = defineModel('widgetRefresh');
-const widgetTimezone = defineModel('widgetTimezone');
+const widgetGlobalSearchButtonPosition = defineModel(
+ 'widgetGlobalSearchButtonPosition',
+);
+const widgetFullscreenButtonPosition = defineModel(
+ 'widgetFullscreenButtonPosition',
+);
+const widgetLanguageToggleButtonPosition = defineModel(
+ 'widgetLanguageToggleButtonPosition',
+);
+const widgetNotificationButtonPosition = defineModel(
+ 'widgetNotificationButtonPosition',
+);
+const widgetThemeToggleButtonPosition = defineModel(
+ 'widgetThemeToggleButtonPosition',
+);
+const widgetLockScreenButtonPosition = defineModel(
+ 'widgetLockScreenButtonPosition',
+);
+const widgetLogoutButtonPosition = defineModel(
+ 'widgetLogoutButtonPosition',
+);
+const widgetRefreshButtonPosition = defineModel(
+ 'widgetRefreshButtonPosition',
+);
+const widgetTimezoneButtonPosition = defineModel(
+ 'widgetTimezoneButtonPosition',
+);
const {
customPreferences,
@@ -483,15 +501,33 @@ function handleCustomPreferencesUpdate(updates: CustomPreferencesRecord) {
v-model:app-preferences-button-position="
appPreferencesButtonPosition
"
- v-model:widget-fullscreen="widgetFullscreen"
- v-model:widget-global-search="widgetGlobalSearch"
- v-model:widget-language-toggle="widgetLanguageToggle"
- v-model:widget-lock-screen="widgetLockScreen"
- v-model:widget-notification="widgetNotification"
- v-model:widget-refresh="widgetRefresh"
- v-model:widget-sidebar-toggle="widgetSidebarToggle"
- v-model:widget-theme-toggle="widgetThemeToggle"
- v-model:widget-timezone="widgetTimezone"
+ v-model:widget-fullscreen-button-position="
+ widgetFullscreenButtonPosition
+ "
+ v-model:widget-global-search-button-position="
+ widgetGlobalSearchButtonPosition
+ "
+ v-model:widget-language-toggle-button-position="
+ widgetLanguageToggleButtonPosition
+ "
+ v-model:widget-lock-screen-button-position="
+ widgetLockScreenButtonPosition
+ "
+ v-model:widget-logout-button-position="
+ widgetLogoutButtonPosition
+ "
+ v-model:widget-notification-button-position="
+ widgetNotificationButtonPosition
+ "
+ v-model:widget-refresh-button-position="
+ widgetRefreshButtonPosition
+ "
+ v-model:widget-theme-toggle-button-position="
+ widgetThemeToggleButtonPosition
+ "
+ v-model:widget-timezone-button-position="
+ widgetTimezoneButtonPosition
+ "
/>
diff --git a/packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue b/packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue
index 2047b950..7a0f293a 100644
--- a/packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue
+++ b/packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue
@@ -3,13 +3,27 @@ import type { Component } from 'vue';
import type { AnyFunction } from '@vben/types';
-import { useTemplateRef, watch } from 'vue';
+import { computed, useTemplateRef, watch } from 'vue';
import { useHoverToggle } from '@vben/hooks';
-import { Settings } from '@vben/icons';
+import {
+ Bell,
+ Fullscreen,
+ Languages,
+ LockKeyhole,
+ LogOut,
+ RotateCw,
+ Search,
+ Settings,
+ Sun,
+ SunMoon,
+} from '@vben/icons';
import { $t } from '@vben/locales';
-import { usePreferences } from '@vben/preferences';
+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,
@@ -17,11 +31,15 @@ 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 {
@@ -70,9 +88,22 @@ const props = withDefaults(defineProps(), {
hoverDelay: 500,
});
-const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
+const emit = defineEmits<{ clearPreferencesAndLogout: []; logout: [] }>();
-const { preferencesButtonPosition } = usePreferences();
+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');
@@ -96,13 +127,157 @@ watch(
},
);
+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();
+ }
+ });
+ }
+}
+
+
+
+ {{ $t('ui.widgets.logoutTip') }}
+
+
{{ menu.text }}
-
+
+
+
+
+ {{ $t('ui.widgets.lockScreen.title') }}
+
+ {{ altView }} L
+
+
+
+
+
+ {{ $t('common.logout') }}
+
+ {{ altView }} Q
+
+
+
+
+
+
+
+ {{ $t('preferences.widget.globalSearch') }}
+
+
+
+ {{ $t('preferences.widget.themeToggle') }}
+
+
+
+ {{ $t('preferences.widget.languageToggle') }}
+
+
+
+ {{ $t('preferences.widget.timezone') }}
+
+
+
+ {{ $t('preferences.widget.fullscreen') }}
+
+
+
+ {{ $t('preferences.widget.notification') }}
+
+
+
+ {{ $t('preferences.widget.refresh') }}
+
+
+