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/package.json b/package.json index f304b751..05dc5a3b 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@changesets/cli": "catalog:", "@tsdown/css": "catalog:", "@types/node": "catalog:", + "@types/sortablejs": "catalog:", "@vben/commitlint-config": "workspace:*", "@vben/eslint-config": "workspace:*", "@vben/oxfmt-config": "workspace:*", @@ -106,5 +107,8 @@ "node": "^22.18.0 || ^24.0.0", "pnpm": ">=11.0.0" }, - "packageManager": "pnpm@11.7.0" + "packageManager": "pnpm@11.7.0", + "dependencies": { + "sortablejs": "catalog:" + } } diff --git a/packages/@core/base/typings/src/app.d.ts b/packages/@core/base/typings/src/app.d.ts index dc6081b1..420ca800 100644 --- a/packages/@core/base/typings/src/app.d.ts +++ b/packages/@core/base/typings/src/app.d.ts @@ -10,16 +10,18 @@ type LayoutType = type ThemeModeType = 'auto' | 'dark' | 'light'; /** - * 按钮位置 - * user-dropdown 用户的下拉弹出框中 - * fixed 固定在右侧 + * 偏好设置按钮位置 + * auto 自动(按布局上下文在 header/fixed 间切换) + * fixed 固定在屏幕右边缘 * header 顶栏 - * auto 自动 + * user-dropdown 用户的下拉弹出框中 + * none 不显示 */ type PreferencesButtonPositionType = | 'auto' | 'fixed' | 'header' + | 'none' | 'user-dropdown'; type BuiltinThemeType = diff --git a/packages/@core/preferences/src/config.ts b/packages/@core/preferences/src/config.ts index 9160a870..0c47ad1b 100644 --- a/packages/@core/preferences/src/config.ts +++ b/packages/@core/preferences/src/config.ts @@ -137,14 +137,34 @@ 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', + order: [ + 'globalSearch', + 'preferences', + 'themeToggle', + 'languageToggle', + 'timezone', + 'fullscreen', + 'notification', + 'lockScreenBtn', + 'logoutBtn', + ], }, }; diff --git a/packages/@core/preferences/src/preferences.ts b/packages/@core/preferences/src/preferences.ts index c7f5caa3..80677da0 100644 --- a/packages/@core/preferences/src/preferences.ts +++ b/packages/@core/preferences/src/preferences.ts @@ -11,7 +11,11 @@ import type { import { markRaw, reactive, readonly, watch } from 'vue'; import { StorageManager } from '@vben-core/shared/cache'; -import { isMacOs, merge } from '@vben-core/shared/utils'; +import { + isMacOs, + merge, + mergeWithArrayOverride, +} from '@vben-core/shared/utils'; import { breakpointsTailwind, @@ -133,7 +137,11 @@ class PreferenceManager { // 加载缓存的偏好设置,并仅用缓存补齐初始化配置中未显式设置的字段 const cachedPreferences = (await this.loadFromCache()) || {}; - const mergedPreference = merge( + // 修复历史脏数据:早期版本用 defu(merge) 合并数组,导致 widget.order + // 在每次 updatePreferences 时被 concat 追加,膨胀到几十/上百条重复 key。 + // 这里在加载缓存后做一次去重,保留首次出现的 key。 + this.sanitizeCachedArray(cachedPreferences, 'widget', 'order'); + const mergedPreference = mergeWithArrayOverride( {}, cachedPreferences, // 用户缓存的设置优先 this.initialPreferences, // 初始设置仅补齐缺失字段 @@ -144,7 +152,7 @@ class PreferenceManager { const cachedCustom = (await this.loadCustomFromCache()) || {}; this.replaceCustomPreferences( - merge( + mergeWithArrayOverride( {}, this.sanitizeCustomPreferences(cachedCustom), this.initialCustomPreferences, @@ -194,7 +202,7 @@ class PreferenceManager { } this.replaceCustomPreferences( - merge({}, sanitizedUpdates, markRaw(this.customState)), + mergeWithArrayOverride({}, sanitizedUpdates, markRaw(this.customState)), ); this.debouncedSave(); }; @@ -205,7 +213,14 @@ class PreferenceManager { */ updatePreferences = (updates: DeepPartial) => { // 深度合并更新内容和当前状态 - const mergedState = merge({}, updates, markRaw(this.state)); + // 注意:必须用 mergeWithArrayOverride 而不是 merge。 + // defu(merge) 对数组的语义是 concat 追加,会导致 widget.order 等 + // 数组字段在每次 updatePreferences 时被重复追加,指数级膨胀。 + const mergedState = mergeWithArrayOverride( + {}, + updates, + markRaw(this.state), + ); Object.assign(this.state, mergedState); // 根据更新的值执行更新 @@ -361,6 +376,28 @@ class PreferenceManager { : undefined; } + /** + * 清理缓存中膨胀的数组字段(保留首次出现的元素) + * 用于修复早期版本 defu 合并数组时 concat 追加导致的脏数据 + */ + private sanitizeCachedArray( + cached: Record, + group: string, + field: string, + ) { + const node = cached?.[group]?.[field]; + if (!Array.isArray(node) || node.length <= 1) return; + const seen = new Set(); + const deduped = node.filter((item) => { + if (seen.has(item)) return false; + seen.add(item); + return true; + }); + if (deduped.length !== node.length) { + cached[group][field] = deduped; + } + } + private sanitizeCustomPreferences( updates: DeepPartial, ) { diff --git a/packages/@core/preferences/src/types.ts b/packages/@core/preferences/src/types.ts index 5834c34c..9d628dbc 100644 --- a/packages/@core/preferences/src/types.ts +++ b/packages/@core/preferences/src/types.ts @@ -365,22 +365,42 @@ 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'; + /** 小部件排序 */ + order: readonly string[]; } interface Preferences { diff --git a/packages/effects/layouts/src/basic/header/header.vue b/packages/effects/layouts/src/basic/header/header.vue index 8b727d51..69e96b10 100644 --- a/packages/effects/layouts/src/basic/header/header.vue +++ b/packages/effects/layouts/src/basic/header/header.vue @@ -121,71 +121,77 @@ type SlotItem = { index: number; name: string }; const rightSlots = computed(() => { const list: Array = []; - // 全局搜索 - if (preferences.widget.globalSearch) { - list.push({ - index: REFERENCE_VALUE, - name: 'global-search', - }); - } - // 偏好设置快捷功能 - if (preferencesButtonPosition.value.header) { - list.push({ - index: REFERENCE_VALUE + 10, - name: 'preferences', - }); - } - // 主题、语言、时区等子功能由各自的 widget 开关独立控制, - // 不应跟随偏好设置按钮的显示与否(如 enablePreferences 为 false 或按钮位于其他位置时) - if (preferences.widget.themeToggle) { - list.push({ - index: REFERENCE_VALUE + 20, - name: 'theme-toggle', - }); - } - if (preferences.widget.languageToggle) { - list.push({ - index: REFERENCE_VALUE + 30, - name: 'language-toggle', - }); - } - if (preferences.widget.timezone) { - list.push({ - index: REFERENCE_VALUE + 40, - name: 'timezone', - }); - } - // 全屏 - if (preferences.widget.fullscreen) { - list.push({ - index: REFERENCE_VALUE + 50, - name: 'fullscreen', - }); - } - // 消息通知 - if (preferences.widget.notification) { - list.push({ - index: REFERENCE_VALUE + 60, - 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', - }); + // 按 widget.order 顺序遍历,检查每个部件是否应在 header 中显示 + const widgetChecks: Record = { + globalSearch: { + visible: + preferences.widget.globalSearch && + preferences.widget.globalSearchButtonPosition === 'header', + slotName: 'global-search', + }, + preferences: { + visible: preferencesButtonPosition.value.header, + slotName: 'preferences', + }, + themeToggle: { + visible: + preferences.widget.themeToggle && + preferences.widget.themeToggleButtonPosition === 'header', + slotName: 'theme-toggle', + }, + languageToggle: { + visible: + preferences.widget.languageToggle && + preferences.widget.languageToggleButtonPosition === 'header', + slotName: 'language-toggle', + }, + timezone: { + visible: + preferences.widget.timezone && + preferences.widget.timezoneButtonPosition === 'header', + slotName: 'timezone', + }, + fullscreen: { + visible: + preferences.widget.fullscreen && + preferences.widget.fullscreenButtonPosition === 'header', + slotName: 'fullscreen', + }, + notification: { + visible: + preferences.widget.notification && + preferences.widget.notificationButtonPosition === 'header', + slotName: 'notification', + }, + lockScreenBtn: { + visible: + preferences.widget.lockScreen && + preferences.widget.lockScreenButtonPosition === 'header', + slotName: 'lock-screen-btn', + }, + logoutBtn: { + visible: preferences.widget.logoutButtonPosition === 'header', + slotName: 'logout-btn', + }, + refresh: { + visible: + preferences.widget.refresh && + preferences.widget.refreshButtonPosition === 'header', + slotName: 'refresh', + }, + }; + + for (const key of preferences.widget.order) { + const check = widgetChecks[key]; + if (check?.visible) { + list.push({ index: REFERENCE_VALUE + list.length, name: check.slotName }); + } + } + + // 用户插槽(header-right-N)追加在后面 Object.keys(slots).forEach((key) => { - // 适配插槽名称,例如第一个插槽名:header-right-1 if (key.startsWith('header-right')) { - // 取第三个占位的数字,若是第三个占位不是数字,则自动分配排序索引 const slotIndex = Number(key.split('-')[2]); const index = Number.isNaN(slotIndex) ? nextIndex(list) : slotIndex; list.push({ index, name: key }); @@ -200,13 +206,6 @@ const rightSlots = computed(() => { const leftSlots = computed(() => { const list: Array = []; - // 刷新 - if (preferences.widget.refresh) { - list.push({ - index: 0, - name: 'refresh', - }); - } Object.keys(slots).forEach((key) => { // 适配插槽名称,例如第一个插槽名:header-left-1 @@ -331,6 +330,15 @@ function clearPreferencesAndLogout() { + + + + + diff --git a/packages/effects/layouts/src/basic/layout.vue b/packages/effects/layouts/src/basic/layout.vue index e83f5a4c..d8646a6c 100644 --- a/packages/effects/layouts/src/basic/layout.vue +++ b/packages/effects/layouts/src/basic/layout.vue @@ -369,9 +369,6 @@ const headerSlots = computed(() => { - - - diff --git a/packages/effects/layouts/src/widgets/preferences/blocks/draggable-list.vue b/packages/effects/layouts/src/widgets/preferences/blocks/draggable-list.vue new file mode 100644 index 00000000..db85f45f --- /dev/null +++ b/packages/effects/layouts/src/widgets/preferences/blocks/draggable-list.vue @@ -0,0 +1,140 @@ + + + + + + + + {{ item.label }} + setPosition(item.key, e)" + > + + {{ opt.label }} + + + + + + + {{ $t('preferences.widget.hidden') }} + + + + {{ item.label }} + + setPosition(item.key, e)" + > + + {{ opt.label }} + + + + + + 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..e0b084a9 100644 --- a/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue +++ b/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue @@ -1,31 +1,70 @@ - - {{ $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.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..c1b6ec8e 100644 --- a/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue +++ b/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue @@ -174,15 +174,34 @@ 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 widgetOrder = defineModel('widgetOrder', { required: true }); +const widgetRefreshButtonPosition = defineModel( + 'widgetRefreshButtonPosition', +); +const widgetTimezoneButtonPosition = defineModel( + 'widgetTimezoneButtonPosition', +); const { customPreferences, @@ -483,15 +502,34 @@ 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-order="widgetOrder" + 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') }} + + +