diff --git a/apps/web-antd/src/layouts/basic.vue b/apps/web-antd/src/layouts/basic.vue index 1bb07ec8f..c938f09cd 100644 --- a/apps/web-antd/src/layouts/basic.vue +++ b/apps/web-antd/src/layouts/basic.vue @@ -226,6 +226,7 @@ watch( description="ann.vben@gmail.com" tag-text="Pro" @logout="handleLogout" + @clear-preferences-and-logout="handleLogout" /> diff --git a/apps/web-antdv-next/src/layouts/basic.vue b/apps/web-antdv-next/src/layouts/basic.vue index 1bb07ec8f..c938f09cd 100644 --- a/apps/web-antdv-next/src/layouts/basic.vue +++ b/apps/web-antdv-next/src/layouts/basic.vue @@ -226,6 +226,7 @@ watch( description="ann.vben@gmail.com" tag-text="Pro" @logout="handleLogout" + @clear-preferences-and-logout="handleLogout" /> diff --git a/apps/web-ele/src/layouts/basic.vue b/apps/web-ele/src/layouts/basic.vue index 1bb07ec8f..c938f09cd 100644 --- a/apps/web-ele/src/layouts/basic.vue +++ b/apps/web-ele/src/layouts/basic.vue @@ -226,6 +226,7 @@ watch( description="ann.vben@gmail.com" tag-text="Pro" @logout="handleLogout" + @clear-preferences-and-logout="handleLogout" /> diff --git a/apps/web-naive/src/layouts/basic.vue b/apps/web-naive/src/layouts/basic.vue index 1bb07ec8f..c938f09cd 100644 --- a/apps/web-naive/src/layouts/basic.vue +++ b/apps/web-naive/src/layouts/basic.vue @@ -226,6 +226,7 @@ watch( description="ann.vben@gmail.com" tag-text="Pro" @logout="handleLogout" + @clear-preferences-and-logout="handleLogout" /> diff --git a/apps/web-tdesign/src/layouts/basic.vue b/apps/web-tdesign/src/layouts/basic.vue index 1bb07ec8f..c938f09cd 100644 --- a/apps/web-tdesign/src/layouts/basic.vue +++ b/apps/web-tdesign/src/layouts/basic.vue @@ -226,6 +226,7 @@ watch( description="ann.vben@gmail.com" tag-text="Pro" @logout="handleLogout" + @clear-preferences-and-logout="handleLogout" /> diff --git a/packages/@core/base/typings/src/app.d.ts b/packages/@core/base/typings/src/app.d.ts index f2b443359..dc6081b1a 100644 --- a/packages/@core/base/typings/src/app.d.ts +++ b/packages/@core/base/typings/src/app.d.ts @@ -10,12 +10,17 @@ type LayoutType = type ThemeModeType = 'auto' | 'dark' | 'light'; /** - * 偏好设置按钮位置 + * 按钮位置 + * user-dropdown 用户的下拉弹出框中 * fixed 固定在右侧 * header 顶栏 * auto 自动 */ -type PreferencesButtonPositionType = 'auto' | 'fixed' | 'header'; +type PreferencesButtonPositionType = + | 'auto' + | 'fixed' + | 'header' + | 'user-dropdown'; type BuiltinThemeType = | 'custom' diff --git a/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap b/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap index 5e3dc6c81..041dfab27 100644 --- a/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap @@ -30,6 +30,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj "loginExpiredMode": "page", "name": "Vben Admin", "preferencesButtonPosition": "auto", + "timezone": "Asia/Shanghai", "watermark": false, "watermarkContent": "", "zIndex": 200, diff --git a/packages/@core/preferences/src/config.ts b/packages/@core/preferences/src/config.ts index c39366a75..88ba861fb 100644 --- a/packages/@core/preferences/src/config.ts +++ b/packages/@core/preferences/src/config.ts @@ -30,6 +30,7 @@ const defaultPreferences: Preferences = { loginExpiredMode: 'page', name: 'Vben Admin', preferencesButtonPosition: 'auto', + timezone: 'Asia/Shanghai', watermark: false, watermarkContent: '', zIndex: 200, diff --git a/packages/@core/preferences/src/preferences.ts b/packages/@core/preferences/src/preferences.ts index 799f3c1fd..8e4cf4b5b 100644 --- a/packages/@core/preferences/src/preferences.ts +++ b/packages/@core/preferences/src/preferences.ts @@ -124,19 +124,19 @@ class PreferenceManager { // 使用命名空间初始化存储管理器 this.cache = new StorageManager({ prefix: namespace }); - // 合并初始偏好设置 + // 合并初始偏好设置:前面的对象优先,后面的对象仅补齐缺失字段 this.initialPreferences = merge({}, overrides, defaultPreferences); this.customPreferencesExtension = extension ?? null; this.initialCustomPreferences = this.resolveCustomPreferencesDefaults( this.customPreferencesExtension, ); - // 加载缓存的偏好设置并与初始配置合并 + // 加载缓存的偏好设置,并仅用缓存补齐初始化配置中未显式设置的字段 const cachedPreferences = (await this.loadFromCache()) || {}; const mergedPreference = merge( {}, + this.initialPreferences, // 初始化配置优先,缓存仅补齐缺失字段 cachedPreferences, - this.initialPreferences, ); // 更新偏好设置 diff --git a/packages/@core/preferences/src/types.ts b/packages/@core/preferences/src/types.ts index 50aefa6e0..bf86935bf 100644 --- a/packages/@core/preferences/src/types.ts +++ b/packages/@core/preferences/src/types.ts @@ -155,6 +155,10 @@ interface AppPreferences { name: string; /** 偏好设置按钮位置 */ preferencesButtonPosition: PreferencesButtonPositionType; + /** + * @zh_CN 应用时区 + */ + timezone: string; /** * @zh_CN 是否开启水印 */ diff --git a/packages/@core/preferences/src/use-preferences.ts b/packages/@core/preferences/src/use-preferences.ts index e126cd9a8..a27f8d733 100644 --- a/packages/@core/preferences/src/use-preferences.ts +++ b/packages/@core/preferences/src/use-preferences.ts @@ -195,12 +195,12 @@ function usePreferences() { */ const preferencesButtonPosition = computed(() => { const { enablePreferences, preferencesButtonPosition } = preferences.app; - // 如果没有启用偏好设置按钮 if (!enablePreferences) { return { fixed: false, header: false, + userDropdown: false, }; } @@ -211,12 +211,15 @@ function usePreferences() { const contentIsMaximize = headerHidden && sidebarHidden; const isHeaderPosition = preferencesButtonPosition === 'header'; + const isUserDropdownPosition = + preferencesButtonPosition === 'user-dropdown'; // 如果设置了固定位置 if (preferencesButtonPosition !== 'auto') { return { fixed: preferencesButtonPosition === 'fixed', header: isHeaderPosition, + userDropdown: isUserDropdownPosition, }; } @@ -230,6 +233,7 @@ function usePreferences() { return { fixed, header: !fixed, + userDropdown: !fixed && isUserDropdownPosition, }; }); diff --git a/packages/effects/layouts/src/basic/header/header.vue b/packages/effects/layouts/src/basic/header/header.vue index b8332822d..1bc8d7f73 100644 --- a/packages/effects/layouts/src/basic/header/header.vue +++ b/packages/effects/layouts/src/basic/header/header.vue @@ -33,52 +33,61 @@ withDefaults(defineProps(), { const emit = defineEmits<{ clearPreferencesAndLogout: [] }>(); -const REFERENCE_VALUE = 50; +const REFERENCE_VALUE = 100; const accessStore = useAccessStore(); const { globalSearchShortcutKey, preferencesButtonPosition } = usePreferences(); const slots = useSlots(); const { refresh } = useRefresh(); +/** + * 插槽列表类型 + */ +type SlotItem = { index: number; name: string }; + const rightSlots = computed(() => { - const list = [{ index: REFERENCE_VALUE + 100, name: 'user-dropdown' }]; + 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', }); + // 将偏好设置中的子功能分组到同一个按钮位置控制逻辑下 + 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.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, @@ -87,17 +96,24 @@ const rightSlots = computed(() => { } Object.keys(slots).forEach((key) => { - const name = key.split('-'); + // 适配插槽名称,例如第一个插槽名:header-right-1 if (key.startsWith('header-right')) { - list.push({ index: Number(name[2]), name: key }); + // 取第三个占位的数字,若是第三个占位不是数字,则自动分配排序索引 + const slotIndex = Number(key.split('-')[2]); + const index = Number.isNaN(slotIndex) ? nextIndex(list) : slotIndex; + list.push({ index, name: key }); } }); + // 最后追加用户下拉框,若是索引值超过1000时则固定在1000(适配用户按钮不在最后的场景) + const userDropdownIndex = Math.min(1000, nextIndex(list)); + list.push({ index: userDropdownIndex, name: 'user-dropdown' }); + // 按照索引排序,保证插槽顺序 return list.toSorted((a, b) => a.index - b.index); }); const leftSlots = computed(() => { - const list: Array<{ index: number; name: string }> = []; - + const list: Array = []; + // 刷新 if (preferences.widget.refresh) { list.push({ index: 0, @@ -106,14 +122,28 @@ const leftSlots = computed(() => { } Object.keys(slots).forEach((key) => { - const name = key.split('-'); + // 适配插槽名称,例如第一个插槽名:header-left-1 if (key.startsWith('header-left')) { - list.push({ index: Number(name[2]), name: key }); + // 取第三个占位的数字,若是第三个占位不是数字,则自动分配排序索引 + const slotIndex = Number(key.split('-')[2]); + const index = Number.isNaN(slotIndex) ? nextIndex(list) : slotIndex; + list.push({ index, name: key }); } }); + // 按照索引排序,保证插槽顺序 return list.toSorted((a, b) => a.index - b.index); }); +/** + * 获取列表下一个索引值(用于排序) + * @param list 列表 + */ +function nextIndex(list: Array) { + const index = + list.length > 0 ? Math.max(...list.map((item) => item.index)) : 0; + return index + 1; +} + function clearPreferencesAndLogout() { emit('clearPreferencesAndLogout'); } diff --git a/packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue b/packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue index 857902924..d5fd1c737 100644 --- a/packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue +++ b/packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue @@ -1,6 +1,9 @@ {{ $t('preferences.language') }} + + {{ $t('preferences.timezone') }} + {{ $t('preferences.dynamicTitle') }} 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 73b7af7a8..39cd14e5b 100644 --- a/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue +++ b/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue @@ -38,6 +38,10 @@ const positionItems = computed((): SelectOption[] => [ label: $t('preferences.position.fixed'), value: 'fixed', }, + { + label: $t('preferences.position.userDropdown'), + value: 'user-dropdown', + }, ]); diff --git a/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue b/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue index c3204166f..963e9da4b 100644 --- a/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue +++ b/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue @@ -65,6 +65,7 @@ const emit = defineEmits<{ clearPreferencesAndLogout: [] }>(); const message = globalShareState.getMessage(); const appLocale = defineModel('appLocale'); +const appTimezone = defineModel('appTimezone'); const appDynamicTitle = defineModel('appDynamicTitle'); const appLayout = defineModel('appLayout'); const appColorGrayMode = defineModel('appColorGrayMode'); @@ -359,6 +360,7 @@ function handleCustomPreferencesUpdate(updates: CustomPreferencesRecord) { v-model:app-enable-check-updates="appEnableCheckUpdates" v-model:app-enable-copy-preferences="appEnableCopyPreferences" v-model:app-locale="appLocale" + v-model:app-timezone="appTimezone" v-model:app-watermark="appWatermark" v-model:app-watermark-content="appWatermarkContent" /> diff --git a/packages/effects/layouts/src/widgets/preferences/preferences.vue b/packages/effects/layouts/src/widgets/preferences/preferences.vue index d99e0d90d..ef2e41cfe 100644 --- a/packages/effects/layouts/src/widgets/preferences/preferences.vue +++ b/packages/effects/layouts/src/widgets/preferences/preferences.vue @@ -11,10 +11,26 @@ import { VbenButton } from '@vben-core/shadcn-ui'; import PreferencesDrawer from './preferences-drawer.vue'; +interface Props { + /** 是否显示按钮 */ + showButton?: boolean; +} + +const props = withDefaults(defineProps(), { + showButton: true, +}); + +const emit = defineEmits<{ clearPreferencesAndLogout: [] }>(); + const [Drawer, drawerApi] = useVbenDrawer({ connectedComponent: PreferencesDrawer, }); +// 暴露打开抽屉的方法 +defineExpose({ + open: () => drawerApi.open(), +}); + /** * preferences 转成 vue props * preferences.widget.fullscreen=>widgetFullscreen @@ -56,17 +72,22 @@ const listen = computed(() => { - + - drawerApi.open()"> - - - - - - + + + drawerApi.open()" + > + + + 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 b11f3857b..8eadde33a 100644 --- a/packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue +++ b/packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue @@ -6,7 +6,7 @@ import type { AnyFunction } from '@vben/types'; import { computed, useTemplateRef, watch } from 'vue'; import { useHoverToggle } from '@vben/hooks'; -import { LockKeyhole, LogOut } from '@vben/icons'; +import { LockKeyhole, LogOut, Settings } from '@vben/icons'; import { $t } from '@vben/locales'; import { preferences, usePreferences } from '@vben/preferences'; import { useAccessStore } from '@vben/stores'; @@ -29,6 +29,7 @@ import { import { useMagicKeys, whenever } from '@vueuse/core'; import { LockScreenModal } from '../lock-screen'; +import { Preferences } from '../preferences'; interface Props { /** @@ -82,10 +83,13 @@ const props = withDefaults(defineProps(), { hoverDelay: 500, }); -const emit = defineEmits<{ logout: [] }>(); +const emit = defineEmits<{ clearPreferencesAndLogout: []; logout: [] }>(); -const { globalLockScreenShortcutKey, globalLogoutShortcutKey } = - usePreferences(); +const { + globalLockScreenShortcutKey, + globalLogoutShortcutKey, + preferencesButtonPosition, +} = usePreferences(); const accessStore = useAccessStore(); const [LockModal, lockModalApi] = useVbenModal({ connectedComponent: LockScreenModal, @@ -98,6 +102,7 @@ const [LogoutModal, logoutModalApi] = useVbenModal({ const refTrigger = useTemplateRef('refTrigger'); const refContent = useTemplateRef('refContent'); +const refPreferences = useTemplateRef('refPreferences'); const [openPopover, hoverWatcher] = useHoverToggle( [refTrigger, refContent], () => props.hoverDelay, @@ -151,6 +156,11 @@ function handleSubmitLogout() { logoutModalApi.close(); } +// 设置 - 打开偏好设置抽屉 +function handleOpenSettings() { + refPreferences.value?.open(); +} + if (enableShortcutKey.value) { const keys = useMagicKeys(); const logoutKey = keys['Alt+KeyQ']; @@ -195,6 +205,13 @@ if (enableShortcutKey.value) { {{ $t('ui.widgets.logoutTip') }} + + @@ -241,6 +258,14 @@ if (enableShortcutKey.value) { {{ menu.text }} + + + {{ $t('preferences.title') }} + { tag-text="Pro" trigger="both" @logout="handleLogout" + @clear-preferences-and-logout="handleLogout" />