Prechádzať zdrojové kódy

feat: supports specifying the position of the preference button (#4154)

Vben 1 rok pred
rodič
commit
30223f18db

+ 3 - 0
docs/src/guide/essentials/settings.md

@@ -189,6 +189,7 @@ const defaultPreferences: Preferences = {
     locale: 'zh-CN',
     loginExpiredMode: 'modal',
     name: 'Vben Admin',
+    preferencesButtonPosition: 'fixed',
     watermark: false,
   },
   breadcrumb: {
@@ -319,6 +320,8 @@ interface AppPreferences {
   loginExpiredMode: LoginExpiredModeType;
   /** 应用名 */
   name: string;
+  /** 偏好设置按钮位置 */
+  preferencesButtonPosition: PreferencesButtonPositionType;
   /**
    * @zh_CN 是否开启水印
    */

+ 1 - 1
packages/@core/base/icons/src/lucide.ts

@@ -38,7 +38,7 @@ export {
   RotateCw,
   Search,
   SearchX,
-  Settings2,
+  Settings,
   Sun,
   SunMoon,
   SwatchBook,

+ 8 - 0
packages/@core/base/typings/src/app.d.ts

@@ -7,6 +7,13 @@ type LayoutType =
 
 type ThemeModeType = 'auto' | 'dark' | 'light';
 
+/**
+ * 偏好设置按钮位置
+ * fixed 固定在右侧
+ * header 顶栏
+ */
+type PreferencesButtonPositionType = 'fixed' | 'header';
+
 type BuiltinThemeType =
   | 'custom'
   | 'deep-blue'
@@ -92,6 +99,7 @@ export type {
   LoginExpiredModeType,
   NavigationStyleType,
   PageTransitionType,
+  PreferencesButtonPositionType,
   TabsStyleType,
   ThemeModeType,
 };

+ 1 - 0
packages/@core/preferences/src/config.ts

@@ -19,6 +19,7 @@ const defaultPreferences: Preferences = {
     locale: 'zh-CN',
     loginExpiredMode: 'modal',
     name: 'Vben Admin',
+    preferencesButtonPosition: 'fixed',
     watermark: false,
   },
   breadcrumb: {

+ 3 - 0
packages/@core/preferences/src/types.ts

@@ -10,6 +10,7 @@ import type {
   LoginExpiredModeType,
   NavigationStyleType,
   PageTransitionType,
+  PreferencesButtonPositionType,
   TabsStyleType,
   ThemeModeType,
 } from '@vben-core/typings';
@@ -49,6 +50,8 @@ interface AppPreferences {
   loginExpiredMode: LoginExpiredModeType;
   /** 应用名 */
   name: string;
+  /** 偏好设置按钮位置 */
+  preferencesButtonPosition: PreferencesButtonPositionType;
   /**
    * @zh_CN 是否开启水印
    */

+ 27 - 7
packages/effects/layouts/src/basic/header/header.vue

@@ -5,7 +5,12 @@ import { preferences, usePreferences } from '@vben/preferences';
 import { useAccessStore } from '@vben/stores';
 import { VbenFullScreen } from '@vben-core/shadcn-ui';
 
-import { GlobalSearch, LanguageToggle, ThemeToggle } from '../../widgets';
+import {
+  GlobalSearch,
+  LanguageToggle,
+  PreferencesButton,
+  ThemeToggle,
+} from '../../widgets';
 
 interface Props {
   /**
@@ -26,34 +31,44 @@ const accessStore = useAccessStore();
 const { globalSearchShortcutKey } = usePreferences();
 const slots = useSlots();
 const rightSlots = computed(() => {
-  const list = [{ index: 30, name: 'user-dropdown' }];
+  const list = [{ index: 100, name: 'user-dropdown' }];
   if (preferences.widget.globalSearch) {
     list.push({
       index: 5,
       name: 'global-search',
     });
   }
-  if (preferences.widget.themeToggle) {
+
+  if (
+    preferences.app.enablePreferences &&
+    preferences.app.preferencesButtonPosition === 'header'
+  ) {
     list.push({
       index: 10,
+      name: 'preferences',
+    });
+  }
+  if (preferences.widget.themeToggle) {
+    list.push({
+      index: 15,
       name: 'theme-toggle',
     });
   }
   if (preferences.widget.languageToggle) {
     list.push({
-      index: 15,
+      index: 20,
       name: 'language-toggle',
     });
   }
   if (preferences.widget.fullscreen) {
     list.push({
-      index: 20,
+      index: 25,
       name: 'fullscreen',
     });
   }
   if (preferences.widget.notification) {
     list.push({
-      index: 25,
+      index: 30,
       name: 'notification',
     });
   }
@@ -66,6 +81,7 @@ const rightSlots = computed(() => {
   });
   return list.sort((a, b) => a.index - b.index);
 });
+
 const leftSlots = computed(() => {
   const list: any[] = [];
 
@@ -108,8 +124,12 @@ const leftSlots = computed(() => {
             class="mr-4"
           />
         </template>
+
+        <template v-else-if="slot.name === 'preferences'">
+          <PreferencesButton class="mr-2" />
+        </template>
         <template v-else-if="slot.name === 'theme-toggle'">
-          <ThemeToggle class="mr-2" />
+          <ThemeToggle class="mr-2 mt-[2px]" />
         </template>
         <template v-else-if="slot.name === 'language-toggle'">
           <LanguageToggle class="mr-2" />

+ 7 - 1
packages/effects/layouts/src/basic/layout.vue

@@ -320,8 +320,14 @@ const headerSlots = computed(() => {
         <slot v-if="lockStore.isLockScreen" name="lock-screen"></slot>
       </Transition>
 
-      <template v-if="preferences.app.enablePreferences">
+      <template
+        v-if="
+          preferences.app.enablePreferences &&
+          preferences.app.preferencesButtonPosition === 'fixed'
+        "
+      >
         <Preferences
+          class="z-100 fixed bottom-20 right-0"
           @clear-preferences-and-logout="clearPreferencesAndLogout"
         />
       </template>

+ 3 - 3
packages/effects/layouts/src/widgets/preferences/blocks/layout/tabbar.vue

@@ -54,9 +54,6 @@ const styleItems = computed((): SelectOption[] => [
   <SwitchItem v-model="tabbarDragable" :disabled="!tabbarEnable">
     {{ $t('preferences.tabbar.dragable') }}
   </SwitchItem>
-  <SelectItem v-model="tabbarStyleType" :items="styleItems">
-    {{ $t('preferences.tabbar.styleType.title') }}
-  </SelectItem>
   <SwitchItem v-model="tabbarShowIcon" :disabled="!tabbarEnable">
     {{ $t('preferences.tabbar.icon') }}
   </SwitchItem>
@@ -69,4 +66,7 @@ const styleItems = computed((): SelectOption[] => [
   <SwitchItem v-model="tabbarShowMaximize" :disabled="!tabbarEnable">
     {{ $t('preferences.tabbar.showMaximize') }}
   </SwitchItem>
+  <SelectItem v-model="tabbarStyleType" :items="styleItems">
+    {{ $t('preferences.tabbar.styleType.title') }}
+  </SelectItem>
 </template>

+ 22 - 0
packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue

@@ -1,6 +1,11 @@
 <script setup lang="ts">
+import type { SelectOption } from '@vben/types';
+
+import { computed } from 'vue';
+
 import { $t } from '@vben/locales';
 
+import SelectItem from '../select-item.vue';
 import SwitchItem from '../switch-item.vue';
 
 defineOptions({
@@ -14,6 +19,20 @@ const widgetNotification = defineModel<boolean>('widgetNotification');
 const widgetThemeToggle = defineModel<boolean>('widgetThemeToggle');
 const widgetSidebarToggle = defineModel<boolean>('widgetSidebarToggle');
 const widgetLockScreen = defineModel<boolean>('widgetLockScreen');
+const appPreferencesButtonPosition = defineModel<string>(
+  'appPreferencesButtonPosition',
+);
+
+const positionItems = computed((): SelectOption[] => [
+  {
+    label: $t('preferences.position.header'),
+    value: 'header',
+  },
+  {
+    label: $t('preferences.position.fixed'),
+    value: 'fixed',
+  },
+]);
 </script>
 
 <template>
@@ -38,4 +57,7 @@ const widgetLockScreen = defineModel<boolean>('widgetLockScreen');
   <SwitchItem v-model="widgetSidebarToggle">
     {{ $t('preferences.widget.sidebarToggle') }}
   </SwitchItem>
+  <SelectItem v-model="appPreferencesButtonPosition" :items="positionItems">
+    {{ $t('preferences.position.title') }}
+  </SelectItem>
 </template>

+ 1 - 0
packages/effects/layouts/src/widgets/preferences/index.ts

@@ -1,2 +1,3 @@
 export { default as Preferences } from './preferences.vue';
+export { default as PreferencesButton } from './preferences-button.vue';
 export * from './use-open-preferences';

+ 13 - 0
packages/effects/layouts/src/widgets/preferences/preferences-button.vue

@@ -0,0 +1,13 @@
+<script lang="ts" setup>
+import { Settings } from '@vben/icons';
+import { VbenIconButton } from '@vben-core/shadcn-ui';
+
+import Preferences from './preferences.vue';
+</script>
+<template>
+  <Preferences>
+    <VbenIconButton>
+      <Settings class="size-4" />
+    </VbenIconButton>
+  </Preferences>
+</template>

+ 17 - 8
packages/effects/layouts/src/widgets/preferences/preferences-sheet.vue

@@ -7,13 +7,14 @@ import type {
   LayoutHeaderModeType,
   LayoutType,
   NavigationStyleType,
+  PreferencesButtonPositionType,
   ThemeModeType,
 } from '@vben/types';
 import type { SegmentedItem } from '@vben-core/shadcn-ui';
 
 import { computed, ref } from 'vue';
 
-import { Copy, RotateCw, Settings2 } from '@vben/icons';
+import { Copy, RotateCw, Settings } from '@vben/icons';
 import { $t, loadLocaleMessages } from '@vben/locales';
 import {
   clearPreferencesCache,
@@ -63,6 +64,9 @@ const appColorWeakMode = defineModel<boolean>('appColorWeakMode');
 const appContentCompact = defineModel<ContentCompactType>('appContentCompact');
 const appWatermark = defineModel<boolean>('appWatermark');
 const appEnableCheckUpdates = defineModel<boolean>('appEnableCheckUpdates');
+const appPreferencesButtonPosition = defineModel<PreferencesButtonPositionType>(
+  'appPreferencesButtonPosition',
+);
 
 const transitionProgress = defineModel<boolean>('transitionProgress');
 const transitionName = defineModel<string>('transitionName');
@@ -220,19 +224,21 @@ async function handleReset() {
 </script>
 
 <template>
-  <div class="z-100 fixed right-0 top-1/2">
+  <div>
     <VbenSheet
       v-model:open="openPreferences"
       :description="$t('preferences.subtitle')"
       :title="$t('preferences.title')"
     >
       <template #trigger>
-        <VbenButton
-          :title="$t('preferences.title')"
-          class="bg-primary flex-col-center h-10 w-10 cursor-pointer rounded-l-lg rounded-r-none border-none"
-        >
-          <Settings2 class="size-5" />
-        </VbenButton>
+        <slot name="trigger">
+          <VbenButton
+            :title="$t('preferences.title')"
+            class="bg-primary flex-col-center size-10 cursor-pointer rounded-l-lg rounded-r-none border-none"
+          >
+            <Settings class="size-5" />
+          </VbenButton>
+        </slot>
       </template>
       <template #extra>
         <div class="flex items-center">
@@ -358,6 +364,9 @@ async function handleReset() {
             </Block>
             <Block :title="$t('preferences.widget.title')">
               <Widget
+                v-model:app-preferences-button-position="
+                  appPreferencesButtonPosition
+                "
                 v-model:widget-fullscreen="widgetFullscreen"
                 v-model:widget-global-search="widgetGlobalSearch"
                 v-model:widget-language-toggle="widgetLanguageToggle"

+ 5 - 1
packages/effects/layouts/src/widgets/preferences/preferences.vue

@@ -47,5 +47,9 @@ const listen = computed(() => {
 });
 </script>
 <template>
-  <PreferencesSheet v-bind="attrs" v-on="listen" />
+  <PreferencesSheet v-bind="attrs" v-on="listen">
+    <template #trigger>
+      <slot></slot>
+    </template>
+  </PreferencesSheet>
 </template>

+ 5 - 0
packages/locales/src/langs/en-US.json

@@ -172,6 +172,11 @@
     "dynamicTitle": "Dynamic Title",
     "watermark": "Watermark",
     "checkUpdates": "Periodic update check",
+    "position": {
+      "title": "Preferences Postion",
+      "header": "Header",
+      "fixed": "Fixed"
+    },
     "sidebar": {
       "title": "Sidebar",
       "width": "Width",

+ 5 - 0
packages/locales/src/langs/zh-CN.json

@@ -172,6 +172,11 @@
     "dynamicTitle": "动态标题",
     "watermark": "水印",
     "checkUpdates": "定时检查更新",
+    "position": {
+      "title": "偏好设置位置",
+      "header": "顶栏",
+      "fixed": "固定"
+    },
     "sidebar": {
       "title": "侧边栏",
       "width": "宽度",