From f840d235ef6357e925029f9dcdae6ed8eccea8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=91=E6=AF=9B88?= Date: Sun, 19 Jul 2026 20:38:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=81=8F=E5=A5=BD=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E5=B0=8F=E9=83=A8=E4=BB=B6=E6=94=AF=E6=8C=81=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E4=B8=8E=E6=8B=96=E6=8B=BD=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 顶栏各小部件按钮支持选择位置(header/user-dropdown/none),偏好设置按钮额外保留 auto/fixed 智能兜底,移动端/全屏内容模式下自动切 fixed 浮动保命,避免偏好入口死锁 - draggable-list.vue 的 listRef 未在模板绑定,Sortable 永不初始化导致小部件无法拖拽;加 ref 绑定与 data-key 让 onEnd 从 DOM 读真实新顺序 - preferences.ts 的 updatePreferences 用 defu(merge) 合并数组,对 widget.order 是 concat 语义,每次拖拽触发更新都会把当前 order 追加一次,localStorage 里膨胀到上百条;改用 mergeWithArrayOverride 整体替换,加载缓存时 sanitizeCachedArray 去重已存在的脏数据 - preferences 按钮之前在 widget.order 里因 labelMap 未登记显示成裸字符串,且 widgetChecks 缺 refresh 导致刷新按钮不按 order 渲染;统一 preferences 与其它 widget 的拖拽排序与位置 select,refresh 补进 rightSlots.widgetChecks 并补模板渲染 - 补 sortablejs 依赖、WidgetPreferences.order 类型字段、preferences-drawer.vue 的 widgetOrder 绑定 --- apps/web-antd/src/layouts/basic.vue | 1 + apps/web-antdv-next/src/layouts/basic.vue | 1 + apps/web-ele/src/layouts/basic.vue | 1 + apps/web-naive/src/layouts/basic.vue | 1 + apps/web-tdesign/src/layouts/basic.vue | 1 + package.json | 6 +- packages/@core/base/typings/src/app.d.ts | 10 +- packages/@core/preferences/src/config.ts | 20 ++ packages/@core/preferences/src/preferences.ts | 47 ++- packages/@core/preferences/src/types.ts | 20 ++ .../layouts/src/basic/header/header.vue | 146 ++++----- packages/effects/layouts/src/basic/layout.vue | 3 - .../preferences/blocks/draggable-list.vue | 140 +++++++++ .../preferences/blocks/layout/widget.vue | 188 +++++++++--- .../preferences/preferences-drawer.vue | 74 +++-- .../widgets/user-dropdown/user-dropdown.vue | 277 +++++++++++++++++- packages/locales/src/langs/en-US/common.json | 3 +- .../locales/src/langs/en-US/preferences.json | 14 +- packages/locales/src/langs/zh-CN/common.json | 3 +- .../locales/src/langs/zh-CN/preferences.json | 14 +- pnpm-lock.yaml | 39 ++- 21 files changed, 848 insertions(+), 161 deletions(-) create mode 100644 packages/effects/layouts/src/widgets/preferences/blocks/draggable-list.vue 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/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 @@ + + + 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 @@ 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(); + } + }); + } +}