Selaa lähdekoodia

perf: simplify the writing of preferences

vince 8 kuukautta sitten
vanhempi
commit
41d0495630

+ 3 - 3
packages/@core/ui-kit/tabs-ui/src/widgets/tool-more.vue

@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import type { DropdownMenuProps } from '@vben-core/shadcn-ui';
 
-import { IcRoundMoreVert } from '@vben-core/iconify';
+import { IcRoundKeyboardArrowDown } from '@vben-core/iconify';
 import { VbenDropdownMenu } from '@vben-core/shadcn-ui';
 
 defineProps<DropdownMenuProps>();
@@ -10,9 +10,9 @@ defineProps<DropdownMenuProps>();
 <template>
   <VbenDropdownMenu :menus="menus" :modal="false">
     <div
-      class="flex-center hover:bg-accent hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-2 text-lg font-semibold"
+      class="flex-center hover:bg-accent hover:text-foreground text-muted-foreground border-border h-full cursor-pointer border-l px-1.5 text-lg font-semibold"
     >
-      <IcRoundMoreVert />
+      <IcRoundKeyboardArrowDown class="size-5" />
     </div>
   </VbenDropdownMenu>
 </template>

+ 2 - 4
packages/business/layouts/src/basic/layout.vue

@@ -12,7 +12,7 @@ import { VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
 import { mapTree } from '@vben-core/toolkit';
 import { MenuRecordRaw } from '@vben-core/typings';
 
-import { Breadcrumb, CozeAssistant, PreferencesWidget } from '../widgets';
+import { Breadcrumb, CozeAssistant, Preferences } from '../widgets';
 import { LayoutContent } from './content';
 import { Copyright } from './copyright';
 import { LayoutFooter } from './footer';
@@ -153,9 +153,7 @@ function clearPreferencesAndLogout() {
     "
   >
     <template v-if="preferences.app.enablePreferences" #preferences>
-      <PreferencesWidget
-        @clear-preferences-and-logout="clearPreferencesAndLogout"
-      />
+      <Preferences @clear-preferences-and-logout="clearPreferencesAndLogout" />
     </template>
 
     <template #floating-groups>

+ 2 - 1
packages/business/layouts/src/widgets/preferences/index.ts

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

+ 405 - 0
packages/business/layouts/src/widgets/preferences/preferences-sheet.vue

@@ -0,0 +1,405 @@
+<script setup lang="ts">
+import type {
+  BuiltinThemeType,
+  ContentCompactType,
+  LayoutHeaderModeType,
+  LayoutType,
+  SupportedLanguagesType,
+  ThemeModeType,
+} from '@vben/types';
+import type {
+  BreadcrumbStyleType,
+  NavigationStyleType,
+} from '@vben-core/preferences';
+import type { SegmentedItem } from '@vben-core/shadcn-ui';
+
+import { computed, ref } from 'vue';
+
+import { IcRoundFolderCopy, IcRoundRestartAlt } from '@vben-core/iconify';
+import { $t, loadLocaleMessages } from '@vben-core/locales';
+import {
+  clearPreferencesCache,
+  preferences,
+  resetPreferences,
+  usePreferences,
+} from '@vben-core/preferences';
+import {
+  VbenButton,
+  VbenIconButton,
+  VbenSegmented,
+  VbenSheet,
+  useToast,
+} from '@vben-core/shadcn-ui';
+
+import { useClipboard } from '@vueuse/core';
+
+import {
+  Animation,
+  Block,
+  Breadcrumb,
+  BuiltinTheme,
+  ColorMode,
+  Content,
+  Copyright,
+  Footer,
+  General,
+  GlobalShortcutKeys,
+  Header,
+  Layout,
+  Navigation,
+  Radius,
+  Sidebar,
+  Tabbar,
+  Theme,
+} from './blocks';
+import IconSetting from './icons/setting.vue';
+import { useOpenPreferences } from './use-open-preferences';
+
+const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
+const { toast } = useToast();
+const appLocale = defineModel<SupportedLanguagesType>('appLocale');
+const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
+const appAiAssistant = defineModel<boolean>('appAiAssistant');
+const appLayout = defineModel<LayoutType>('appLayout');
+const appColorGrayMode = defineModel<boolean>('appColorGrayMode');
+const appColorWeakMode = defineModel<boolean>('appColorWeakMode');
+const appSemiDarkMenu = defineModel<boolean>('appSemiDarkMenu');
+const appContentCompact = defineModel<ContentCompactType>('appContentCompact');
+
+const transitionProgress = defineModel<boolean>('transitionProgress');
+const transitionName = defineModel<string>('transitionName');
+const transitionLoading = defineModel<boolean>('transitionLoading');
+const transitionEnable = defineModel<boolean>('transitionEnable');
+
+const themeColorPrimary = defineModel<string>('themeColorPrimary');
+const themeBuiltinType = defineModel<BuiltinThemeType>('themeBuiltinType');
+const themeMode = defineModel<ThemeModeType>('themeMode');
+const themeRadius = defineModel<string>('themeRadius');
+
+const sidebarEnable = defineModel<boolean>('sidebarEnable');
+const sidebarWidth = defineModel<number>('sidebarWidth');
+const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
+const sidebarCollapsedShowTitle = defineModel<boolean>(
+  'sidebarCollapsedShowTitle',
+);
+
+const headerEnable = defineModel<boolean>('headerEnable');
+const headerMode = defineModel<LayoutHeaderModeType>('headerMode');
+
+const breadcrumbEnable = defineModel<boolean>('breadcrumbEnable');
+const breadcrumbShowIcon = defineModel<boolean>('breadcrumbShowIcon');
+const breadcrumbShowHome = defineModel<boolean>('breadcrumbShowHome');
+const breadcrumbStyleType = defineModel<BreadcrumbStyleType>(
+  'breadcrumbStyleType',
+);
+const breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');
+
+const tabbarEnable = defineModel<boolean>('tabbarEnable');
+const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
+
+const navigationStyleType = defineModel<NavigationStyleType>(
+  'navigationStyleType',
+);
+const navigationSplit = defineModel<boolean>('navigationSplit');
+const navigationAccordion = defineModel<boolean>('navigationAccordion');
+
+// const logoVisible = defineModel<boolean>('logoVisible');
+
+const footerEnable = defineModel<boolean>('footerEnable');
+const footerFixed = defineModel<boolean>('footerFixed');
+
+const copyrightEnable = defineModel<boolean>('copyrightEnable');
+const copyrightCompanyName = defineModel<string>('copyrightCompanyName');
+const copyrightCompanySiteLink = defineModel<string>(
+  'copyrightCompanySiteLink',
+);
+const copyrightDate = defineModel<string>('copyrightDate');
+const copyrightIcp = defineModel<string>('copyrightIcp');
+const copyrightIcpLink = defineModel<string>('copyrightIcpLink');
+
+const shortcutKeysEnable = defineModel<boolean>('shortcutKeysEnable');
+const shortcutKeysGlobalSearch = defineModel<boolean>(
+  'shortcutKeysGlobalSearch',
+);
+const shortcutKeysGlobalLogout = defineModel<boolean>(
+  'shortcutKeysGlobalLogout',
+);
+const shortcutKeysGlobalPreferences = defineModel<boolean>(
+  'shortcutKeysGlobalPreferences',
+);
+
+const {
+  diffPreference,
+  isDark,
+  isFullContent,
+  isHeaderNav,
+  isMixedNav,
+  isSideMixedNav,
+  isSideMode,
+  isSideNav,
+} = usePreferences();
+const { copy } = useClipboard();
+
+const activeTab = ref('appearance');
+
+const tabs = computed((): SegmentedItem[] => {
+  return [
+    {
+      label: $t('preferences.appearance'),
+      value: 'appearance',
+    },
+    {
+      label: $t('preferences.layout'),
+      value: 'layout',
+    },
+    {
+      label: $t('preferences.shortcut-keys.title'),
+      value: 'shortcutKey',
+    },
+    {
+      label: $t('preferences.general'),
+      value: 'general',
+    },
+  ];
+});
+
+const showBreadcrumbConfig = computed(() => {
+  return (
+    !isFullContent.value &&
+    !isMixedNav.value &&
+    !isHeaderNav.value &&
+    preferences.header.enable
+  );
+});
+
+const { openPreferences } = useOpenPreferences();
+
+async function handleCopy() {
+  await copy(JSON.stringify(diffPreference.value, null, 2));
+
+  toast({
+    description: $t('preferences.copy'),
+    title: $t('preferences.copy-success'),
+  });
+}
+
+async function handleClearCache() {
+  resetPreferences();
+  clearPreferencesCache();
+  emit('clearPreferencesAndLogout');
+}
+
+async function handleReset() {
+  if (!diffPreference.value) {
+    return;
+  }
+  resetPreferences();
+  await loadLocaleMessages(preferences.app.locale);
+  toast({
+    description: $t('preferences.reset-title'),
+    title: $t('preferences.reset-success'),
+  });
+  toast({
+    description: $t('preferences.reset-title'),
+    title: $t('preferences.reset-success'),
+  });
+}
+</script>
+
+<template>
+  <div class="z-100 fixed right-0 top-1/2">
+    <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-12 w-12 cursor-pointer rounded-l-lg rounded-r-none border-none"
+        >
+          <IconSetting
+            class="duration-3000 fill-primary-foreground animate-spin text-2xl"
+          />
+        </VbenButton>
+      </template>
+      <template #extra>
+        <div class="flex items-center">
+          <VbenIconButton
+            :disabled="!diffPreference"
+            :tooltip="$t('preferences.reset-tip')"
+            class="relative"
+          >
+            <span
+              v-if="diffPreference"
+              class="bg-primary absolute right-0.5 top-0.5 h-2 w-2 rounded"
+            ></span>
+            <IcRoundRestartAlt class="size-5" @click="handleReset" />
+          </VbenIconButton>
+        </div>
+      </template>
+
+      <div class="p-4 pt-4">
+        <VbenSegmented v-model="activeTab" :tabs="tabs">
+          <template #general>
+            <Block :title="$t('preferences.general')">
+              <General
+                v-model:app-ai-assistant="appAiAssistant"
+                v-model:app-dynamic-title="appDynamicTitle"
+                v-model:app-locale="appLocale"
+              />
+            </Block>
+
+            <Block :title="$t('preferences.animation.title')">
+              <Animation
+                v-model:transition-enable="transitionEnable"
+                v-model:transition-loading="transitionLoading"
+                v-model:transition-name="transitionName"
+                v-model:transition-progress="transitionProgress"
+              />
+            </Block>
+          </template>
+          <template #appearance>
+            <Block :title="$t('preferences.theme.title')">
+              <Theme
+                v-model="themeMode"
+                v-model:app-semi-dark-menu="appSemiDarkMenu"
+              />
+            </Block>
+            <!-- <Block :title="$t('preferences.theme-color')">
+              <ThemeColor
+                v-model="themeColorPrimary"
+                :color-primary-presets="colorPrimaryPresets"
+              />
+            </Block> -->
+            <Block :title="$t('preferences.theme.builtin.title')">
+              <BuiltinTheme
+                v-model="themeBuiltinType"
+                v-model:theme-color-primary="themeColorPrimary"
+                :is-dark="isDark"
+              />
+            </Block>
+            <Block :title="$t('preferences.theme.radius')">
+              <Radius v-model="themeRadius" />
+            </Block>
+            <Block :title="$t('preferences.other')">
+              <ColorMode
+                v-model:app-color-gray-mode="appColorGrayMode"
+                v-model:app-color-weak-mode="appColorWeakMode"
+              />
+            </Block>
+          </template>
+          <template #layout>
+            <Block :title="$t('preferences.layout')">
+              <Layout v-model="appLayout" />
+            </Block>
+            <Block :title="$t('preferences.content')">
+              <Content v-model="appContentCompact" />
+            </Block>
+
+            <Block :title="$t('preferences.sidebar.title')">
+              <Sidebar
+                v-model:sidebar-collapsed="sidebarCollapsed"
+                v-model:sidebar-collapsed-show-title="sidebarCollapsedShowTitle"
+                v-model:sidebar-enable="sidebarEnable"
+                v-model:sidebar-width="sidebarWidth"
+                :disabled="!isSideMode"
+              />
+            </Block>
+
+            <Block :title="$t('preferences.header.title')">
+              <Header
+                v-model:headerEnable="headerEnable"
+                v-model:headerMode="headerMode"
+                :disabled="isFullContent"
+              />
+            </Block>
+
+            <Block :title="$t('preferences.navigation-menu.title')">
+              <Navigation
+                v-model:navigation-accordion="navigationAccordion"
+                v-model:navigation-split="navigationSplit"
+                v-model:navigation-style-type="navigationStyleType"
+                :disabled="isFullContent"
+                :disabled-navigation-split="!isMixedNav"
+              />
+            </Block>
+
+            <Block :title="$t('preferences.breadcrumb.title')">
+              <Breadcrumb
+                v-model:breadcrumb-enable="breadcrumbEnable"
+                v-model:breadcrumb-hide-only-one="breadcrumbHideOnlyOne"
+                v-model:breadcrumb-show-home="breadcrumbShowHome"
+                v-model:breadcrumb-show-icon="breadcrumbShowIcon"
+                v-model:breadcrumb-style-type="breadcrumbStyleType"
+                :disabled="
+                  !showBreadcrumbConfig || !(isSideNav || isSideMixedNav)
+                "
+              />
+            </Block>
+
+            <Block :title="$t('preferences.tabbar.title')">
+              <Tabbar
+                v-model:tabbar-enable="tabbarEnable"
+                v-model:tabbar-show-icon="tabbarShowIcon"
+              />
+            </Block>
+            <Block :title="$t('preferences.footer.title')">
+              <Footer
+                v-model:footer-enable="footerEnable"
+                v-model:footer-fixed="footerFixed"
+              />
+            </Block>
+            <Block :title="$t('preferences.copyright.title')">
+              <Copyright
+                v-model:copyright-company-name="copyrightCompanyName"
+                v-model:copyright-company-site-link="copyrightCompanySiteLink"
+                v-model:copyright-date="copyrightDate"
+                v-model:copyright-enable="copyrightEnable"
+                v-model:copyright-icp="copyrightIcp"
+                v-model:copyright-icp-link="copyrightIcpLink"
+                :disabled="!footerEnable"
+              />
+            </Block>
+          </template>
+
+          <template #shortcutKey>
+            <Block :title="$t('preferences.shortcut-keys.global')">
+              <GlobalShortcutKeys
+                v-model:shortcut-keys-enable="shortcutKeysEnable"
+                v-model:shortcut-keys-global-search="shortcutKeysGlobalSearch"
+                v-model:shortcut-keys-logout="shortcutKeysGlobalLogout"
+                v-model:shortcut-keys-preferences="
+                  shortcutKeysGlobalPreferences
+                "
+              />
+            </Block>
+          </template>
+        </VbenSegmented>
+      </div>
+
+      <template #footer>
+        <VbenButton
+          :disabled="!diffPreference"
+          class="mx-4 w-full"
+          size="sm"
+          variant="outline"
+          @click="handleClearCache"
+        >
+          <IcRoundRestartAlt class="mr-2 size-4" />
+          {{ $t('preferences.clear-and-logout') }}
+        </VbenButton>
+        <VbenButton
+          :disabled="!diffPreference"
+          class="mr-4 w-full"
+          size="sm"
+          variant="default"
+          @click="handleCopy"
+        >
+          <IcRoundFolderCopy class="mr-2 size-3" />
+          {{ $t('preferences.copy') }}
+        </VbenButton>
+      </template>
+    </VbenSheet>
+  </div>
+</template>

+ 0 - 185
packages/business/layouts/src/widgets/preferences/preferences-widget.vue

@@ -1,185 +0,0 @@
-<script lang="ts" setup>
-import { loadLocaleMessages } from '@vben-core/locales';
-import { preferences, updatePreferences } from '@vben-core/preferences';
-
-import Preferences from './preferences.vue';
-</script>
-<template>
-  <Preferences
-    :app-ai-assistant="preferences.app.aiAssistant"
-    :app-color-gray-mode="preferences.app.colorGrayMode"
-    :app-color-weak-mode="preferences.app.colorWeakMode"
-    :app-content-compact="preferences.app.contentCompact"
-    :app-dynamic-title="preferences.app.dynamicTitle"
-    :app-layout="preferences.app.layout"
-    :app-locale="preferences.app.locale"
-    :app-semi-dark-menu="preferences.app.semiDarkMenu"
-    :breadcrumb-enable="preferences.breadcrumb.enable"
-    :breadcrumb-hide-only-one="preferences.breadcrumb.hideOnlyOne"
-    :breadcrumb-show-home="preferences.breadcrumb.showHome"
-    :breadcrumb-show-icon="preferences.breadcrumb.showIcon"
-    :breadcrumb-style-type="preferences.breadcrumb.styleType"
-    :copyright-company-name="preferences.copyright.companyName"
-    :copyright-company-site-link="preferences.copyright.companySiteLink"
-    :copyright-date="preferences.copyright.date"
-    :copyright-enable="preferences.copyright.enable"
-    :copyright-icp="preferences.copyright.icp"
-    :copyright-icp-link="preferences.copyright.icpLink"
-    :footer-enable="preferences.footer.enable"
-    :footer-fixed="preferences.footer.fixed"
-    :header-enable="preferences.header.enable"
-    :header-mode="preferences.header.mode"
-    :navigation-accordion="preferences.navigation.accordion"
-    :navigation-split="preferences.navigation.split"
-    :navigation-style-type="preferences.navigation.styleType"
-    :shortcut-keys-enable="preferences.shortcutKeys.enable"
-    :shortcut-keys-global-logout="preferences.shortcutKeys.globalLogout"
-    :shortcut-keys-global-preferences="
-      preferences.shortcutKeys.globalPreferences
-    "
-    :shortcut-keys-global-search="preferences.shortcutKeys.globalSearch"
-    :sidebar-collapsed="preferences.sidebar.collapsed"
-    :sidebar-collapsed-show-title="preferences.sidebar.collapsedShowTitle"
-    :sidebar-enable="preferences.sidebar.enable"
-    :sidebar-width="preferences.sidebar.width"
-    :tabbar-enable="preferences.tabbar.enable"
-    :tabbar-show-icon="preferences.tabbar.showIcon"
-    :theme-builtin-type="preferences.theme.builtinType"
-    :theme-color-primary="preferences.theme.colorPrimary"
-    :theme-mode="preferences.theme.mode"
-    :theme-radius="preferences.theme.radius"
-    :transition-enable="preferences.transition.enable"
-    :transition-loading="preferences.transition.loading"
-    :transition-name="preferences.transition.name"
-    :transition-progress="preferences.transition.progress"
-    @update:app-ai-assistant="
-      (val) => updatePreferences({ app: { aiAssistant: val } })
-    "
-    @update:app-color-gray-mode="
-      (val) => updatePreferences({ app: { colorGrayMode: val } })
-    "
-    @update:app-color-weak-mode="
-      (val) => updatePreferences({ app: { colorWeakMode: val } })
-    "
-    @update:app-content-compact="
-      (val) => updatePreferences({ app: { contentCompact: val } })
-    "
-    @update:app-dynamic-title="
-      (val) => updatePreferences({ app: { dynamicTitle: val } })
-    "
-    @update:app-layout="(val) => updatePreferences({ app: { layout: val } })"
-    @update:app-locale="
-      (val) => {
-        updatePreferences({ app: { locale: val } });
-        loadLocaleMessages(val);
-      }
-    "
-    @update:app-semi-dark-menu="
-      (val) => updatePreferences({ app: { semiDarkMenu: val } })
-    "
-    @update:breadcrumb-enable="
-      (val) => updatePreferences({ breadcrumb: { enable: val } })
-    "
-    @update:breadcrumb-hide-only-one="
-      (val) => updatePreferences({ breadcrumb: { hideOnlyOne: val } })
-    "
-    @update:breadcrumb-show-home="
-      (val) => updatePreferences({ breadcrumb: { showHome: val } })
-    "
-    @update:breadcrumb-show-icon="
-      (val) => updatePreferences({ breadcrumb: { showIcon: val } })
-    "
-    @update:breadcrumb-style-type="
-      (val) => updatePreferences({ breadcrumb: { styleType: val } })
-    "
-    @update:copyright-company-name="
-      (val) => updatePreferences({ copyright: { companyName: val } })
-    "
-    @update:copyright-company-site-link="
-      (val) => updatePreferences({ copyright: { companySiteLink: val } })
-    "
-    @update:copyright-date="
-      (val) => updatePreferences({ copyright: { date: val } })
-    "
-    @update:copyright-enable="
-      (val) => updatePreferences({ copyright: { enable: val } })
-    "
-    @update:copyright-icp="
-      (val) => updatePreferences({ copyright: { icp: val } })
-    "
-    @update:copyright-icp-link="
-      (val) => updatePreferences({ copyright: { icpLink: val } })
-    "
-    @update:footer-enable="
-      (val) => updatePreferences({ footer: { enable: val } })
-    "
-    @update:footer-fixed="
-      (val) => updatePreferences({ footer: { fixed: val } })
-    "
-    @update:header-enable="
-      (val) => updatePreferences({ header: { enable: val } })
-    "
-    @update:header-mode="(val) => updatePreferences({ header: { mode: val } })"
-    @update:navigation-accordion="
-      (val) => updatePreferences({ navigation: { accordion: val } })
-    "
-    @update:navigation-split="
-      (val) => updatePreferences({ navigation: { split: val } })
-    "
-    @update:navigation-style-type="
-      (val) => updatePreferences({ navigation: { styleType: val } })
-    "
-    @update:shortcut-keys-enable="
-      (val) => updatePreferences({ shortcutKeys: { enable: val } })
-    "
-    @update:shortcut-keys-global-logout="
-      (val) => updatePreferences({ shortcutKeys: { globalLogout: val } })
-    "
-    @update:shortcut-keys-global-preferences="
-      (val) => updatePreferences({ shortcutKeys: { globalPreferences: val } })
-    "
-    @update:shortcut-keys-global-search="
-      (val) => updatePreferences({ shortcutKeys: { globalSearch: val } })
-    "
-    @update:sidebar-collapsed="
-      (val) => updatePreferences({ sidebar: { collapsed: val } })
-    "
-    @update:sidebar-collapsed-show-title="
-      (val) => updatePreferences({ sidebar: { collapsedShowTitle: val } })
-    "
-    @update:sidebar-enable="
-      (val) => updatePreferences({ sidebar: { enable: val } })
-    "
-    @update:sidebar-width="
-      (val) => updatePreferences({ sidebar: { width: val } })
-    "
-    @update:tabbar-enable="
-      (val) => updatePreferences({ tabbar: { enable: val } })
-    "
-    @update:tabbar-show-icon="
-      (val) => updatePreferences({ tabbar: { showIcon: val } })
-    "
-    @update:theme-builtin-type="
-      (val) => updatePreferences({ theme: { builtinType: val } })
-    "
-    @update:theme-color-primary="
-      (val) => updatePreferences({ theme: { colorPrimary: val } })
-    "
-    @update:theme-mode="(val) => updatePreferences({ theme: { mode: val } })"
-    @update:theme-radius="
-      (val) => updatePreferences({ theme: { radius: val } })
-    "
-    @update:transition-enable="
-      (val) => updatePreferences({ transition: { enable: val } })
-    "
-    @update:transition-loading="
-      (val) => updatePreferences({ transition: { loading: val } })
-    "
-    @update:transition-name="
-      (val) => updatePreferences({ transition: { name: val } })
-    "
-    @update:transition-progress="
-      (val) => updatePreferences({ transition: { progress: val } })
-    "
-  />
-</template>

+ 38 - 392
packages/business/layouts/src/widgets/preferences/preferences.vue

@@ -1,398 +1,44 @@
-<script setup lang="ts">
-import type {
-  BuiltinThemeType,
-  ContentCompactType,
-  LayoutHeaderModeType,
-  LayoutType,
-  SupportedLanguagesType,
-  ThemeModeType,
-} from '@vben/types';
-import type {
-  BreadcrumbStyleType,
-  NavigationStyleType,
-} from '@vben-core/preferences';
-import type { SegmentedItem } from '@vben-core/shadcn-ui';
-
-import { computed, ref } from 'vue';
-
-import { IcRoundFolderCopy, IcRoundRestartAlt } from '@vben-core/iconify';
-import { $t, loadLocaleMessages } from '@vben-core/locales';
-import {
-  clearPreferencesCache,
-  preferences,
-  resetPreferences,
-  usePreferences,
-} from '@vben-core/preferences';
-import {
-  VbenButton,
-  VbenIconButton,
-  VbenSegmented,
-  VbenSheet,
-  useToast,
-} from '@vben-core/shadcn-ui';
-
-import { useClipboard } from '@vueuse/core';
-
-import {
-  Animation,
-  Block,
-  Breadcrumb,
-  BuiltinTheme,
-  ColorMode,
-  Content,
-  Copyright,
-  Footer,
-  General,
-  GlobalShortcutKeys,
-  Header,
-  Layout,
-  Navigation,
-  Radius,
-  Sidebar,
-  Tabbar,
-  Theme,
-} from './blocks';
-import Trigger from './trigger.vue';
-import { useOpenPreferences } from './use-open-preferences';
-
-const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
-const { toast } = useToast();
-const appLocale = defineModel<SupportedLanguagesType>('appLocale');
-const appDynamicTitle = defineModel<boolean>('appDynamicTitle');
-const appAiAssistant = defineModel<boolean>('appAiAssistant');
-const appLayout = defineModel<LayoutType>('appLayout');
-const appColorGrayMode = defineModel<boolean>('appColorGrayMode');
-const appColorWeakMode = defineModel<boolean>('appColorWeakMode');
-const appSemiDarkMenu = defineModel<boolean>('appSemiDarkMenu');
-const appContentCompact = defineModel<ContentCompactType>('appContentCompact');
-
-const transitionProgress = defineModel<boolean>('transitionProgress');
-const transitionName = defineModel<string>('transitionName');
-const transitionLoading = defineModel<boolean>('transitionLoading');
-const transitionEnable = defineModel<boolean>('transitionEnable');
-
-const themeColorPrimary = defineModel<string>('themeColorPrimary');
-const themeBuiltinType = defineModel<BuiltinThemeType>('themeBuiltinType');
-const themeMode = defineModel<ThemeModeType>('themeMode');
-const themeRadius = defineModel<string>('themeRadius');
-
-const sidebarEnable = defineModel<boolean>('sidebarEnable');
-const sidebarWidth = defineModel<number>('sidebarWidth');
-const sidebarCollapsed = defineModel<boolean>('sidebarCollapsed');
-const sidebarCollapsedShowTitle = defineModel<boolean>(
-  'sidebarCollapsedShowTitle',
-);
-
-const headerEnable = defineModel<boolean>('headerEnable');
-const headerMode = defineModel<LayoutHeaderModeType>('headerMode');
-
-const breadcrumbEnable = defineModel<boolean>('breadcrumbEnable');
-const breadcrumbShowIcon = defineModel<boolean>('breadcrumbShowIcon');
-const breadcrumbShowHome = defineModel<boolean>('breadcrumbShowHome');
-const breadcrumbStyleType = defineModel<BreadcrumbStyleType>(
-  'breadcrumbStyleType',
-);
-const breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');
-
-const tabbarEnable = defineModel<boolean>('tabbarEnable');
-const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
-
-const navigationStyleType = defineModel<NavigationStyleType>(
-  'navigationStyleType',
-);
-const navigationSplit = defineModel<boolean>('navigationSplit');
-const navigationAccordion = defineModel<boolean>('navigationAccordion');
-
-// const logoVisible = defineModel<boolean>('logoVisible');
-
-const footerEnable = defineModel<boolean>('footerEnable');
-const footerFixed = defineModel<boolean>('footerFixed');
-
-const copyrightEnable = defineModel<boolean>('copyrightEnable');
-const copyrightCompanyName = defineModel<string>('copyrightCompanyName');
-const copyrightCompanySiteLink = defineModel<string>(
-  'copyrightCompanySiteLink',
-);
-const copyrightDate = defineModel<string>('copyrightDate');
-const copyrightIcp = defineModel<string>('copyrightIcp');
-const copyrightIcpLink = defineModel<string>('copyrightIcpLink');
-
-const shortcutKeysEnable = defineModel<boolean>('shortcutKeysEnable');
-const shortcutKeysGlobalSearch = defineModel<boolean>(
-  'shortcutKeysGlobalSearch',
-);
-const shortcutKeysGlobalLogout = defineModel<boolean>(
-  'shortcutKeysGlobalLogout',
-);
-const shortcutKeysGlobalPreferences = defineModel<boolean>(
-  'shortcutKeysGlobalPreferences',
-);
-
-const {
-  diffPreference,
-  isDark,
-  isFullContent,
-  isHeaderNav,
-  isMixedNav,
-  isSideMixedNav,
-  isSideMode,
-  isSideNav,
-} = usePreferences();
-const { copy } = useClipboard();
-
-const activeTab = ref('appearance');
-
-const tabs = computed((): SegmentedItem[] => {
-  return [
-    {
-      label: $t('preferences.appearance'),
-      value: 'appearance',
-    },
-    {
-      label: $t('preferences.layout'),
-      value: 'layout',
-    },
-    {
-      label: $t('preferences.shortcut-keys.title'),
-      value: 'shortcutKey',
-    },
-    {
-      label: $t('preferences.general'),
-      value: 'general',
-    },
-  ];
-});
-
-const showBreadcrumbConfig = computed(() => {
-  return (
-    !isFullContent.value &&
-    !isMixedNav.value &&
-    !isHeaderNav.value &&
-    preferences.header.enable
-  );
+<script lang="ts" setup>
+import { computed } from 'vue';
+
+import { preferences, updatePreferences } from '@vben-core/preferences';
+import { capitalizeFirstLetter } from '@vben-core/toolkit';
+
+import Preferences from './preferences-sheet.vue';
+
+/**
+ * preferences 转成 vue props
+ * preferences.app.aiAssistant=>appAiAssistant
+ */
+const attrs = computed(() => {
+  const result: Record<string, any> = {};
+  for (const [key, value] of Object.entries(preferences)) {
+    for (const [subKey, subValue] of Object.entries(value)) {
+      result[`${key}${capitalizeFirstLetter(subKey)}`] = subValue;
+    }
+  }
+  return result;
 });
 
-const { openPreferences } = useOpenPreferences();
-
-async function handleCopy() {
-  await copy(JSON.stringify(diffPreference.value, null, 2));
-
-  toast({
-    description: $t('preferences.copy'),
-    title: $t('preferences.copy-success'),
-  });
-}
-
-async function handleClearCache() {
-  resetPreferences();
-  clearPreferencesCache();
-  emit('clearPreferencesAndLogout');
-}
-
-async function handleReset() {
-  if (!diffPreference.value) {
-    return;
+/**
+ * preferences 转成 vue listener
+ * preferences.app.aiAssistant=>@update:appAiAssistant
+ */
+const listen = computed(() => {
+  const result: Record<string, any> = {};
+  for (const [key, value] of Object.entries(preferences)) {
+    if (typeof value === 'object') {
+      for (const subKey of Object.keys(value)) {
+        result[`update:${key}${capitalizeFirstLetter(subKey)}`] = (val: any) =>
+          updatePreferences({ [key]: { [subKey]: val } });
+      }
+    } else {
+      result[key] = value;
+    }
   }
-  resetPreferences();
-  await loadLocaleMessages(preferences.app.locale);
-  toast({
-    description: $t('preferences.reset-title'),
-    title: $t('preferences.reset-success'),
-  });
-  toast({
-    description: $t('preferences.reset-title'),
-    title: $t('preferences.reset-success'),
-  });
-}
+  return result;
+});
 </script>
-
 <template>
-  <div class="z-100 fixed right-0 top-1/2">
-    <VbenSheet
-      v-model:open="openPreferences"
-      :description="$t('preferences.subtitle')"
-      :title="$t('preferences.title')"
-    >
-      <template #trigger>
-        <Trigger />
-      </template>
-      <template #extra>
-        <div class="flex items-center">
-          <VbenIconButton
-            :disabled="!diffPreference"
-            :tooltip="$t('preferences.reset-tip')"
-            class="relative"
-          >
-            <span
-              v-if="diffPreference"
-              class="bg-primary absolute right-0.5 top-0.5 h-2 w-2 rounded"
-            ></span>
-            <IcRoundRestartAlt class="size-5" @click="handleReset" />
-          </VbenIconButton>
-        </div>
-      </template>
-
-      <div class="p-4 pt-4">
-        <VbenSegmented v-model="activeTab" :tabs="tabs">
-          <template #general>
-            <Block :title="$t('preferences.general')">
-              <General
-                v-model:app-ai-assistant="appAiAssistant"
-                v-model:app-dynamic-title="appDynamicTitle"
-                v-model:app-locale="appLocale"
-              />
-            </Block>
-
-            <Block :title="$t('preferences.animation.title')">
-              <Animation
-                v-model:transition-enable="transitionEnable"
-                v-model:transition-loading="transitionLoading"
-                v-model:transition-name="transitionName"
-                v-model:transition-progress="transitionProgress"
-              />
-            </Block>
-          </template>
-          <template #appearance>
-            <Block :title="$t('preferences.theme.title')">
-              <Theme
-                v-model="themeMode"
-                v-model:app-semi-dark-menu="appSemiDarkMenu"
-              />
-            </Block>
-            <!-- <Block :title="$t('preferences.theme-color')">
-              <ThemeColor
-                v-model="themeColorPrimary"
-                :color-primary-presets="colorPrimaryPresets"
-              />
-            </Block> -->
-            <Block :title="$t('preferences.theme.builtin.title')">
-              <BuiltinTheme
-                v-model="themeBuiltinType"
-                v-model:theme-color-primary="themeColorPrimary"
-                :is-dark="isDark"
-              />
-            </Block>
-            <Block :title="$t('preferences.theme.radius')">
-              <Radius v-model="themeRadius" />
-            </Block>
-            <Block :title="$t('preferences.other')">
-              <ColorMode
-                v-model:app-color-gray-mode="appColorGrayMode"
-                v-model:app-color-weak-mode="appColorWeakMode"
-              />
-            </Block>
-          </template>
-          <template #layout>
-            <Block :title="$t('preferences.layout')">
-              <Layout v-model="appLayout" />
-            </Block>
-            <Block :title="$t('preferences.content')">
-              <Content v-model="appContentCompact" />
-            </Block>
-
-            <Block :title="$t('preferences.sidebar.title')">
-              <Sidebar
-                v-model:sidebar-collapsed="sidebarCollapsed"
-                v-model:sidebar-collapsed-show-title="sidebarCollapsedShowTitle"
-                v-model:sidebar-enable="sidebarEnable"
-                v-model:sidebar-width="sidebarWidth"
-                :disabled="!isSideMode"
-              />
-            </Block>
-
-            <Block :title="$t('preferences.header.title')">
-              <Header
-                v-model:headerEnable="headerEnable"
-                v-model:headerMode="headerMode"
-                :disabled="isFullContent"
-              />
-            </Block>
-
-            <Block :title="$t('preferences.navigation-menu.title')">
-              <Navigation
-                v-model:navigation-accordion="navigationAccordion"
-                v-model:navigation-split="navigationSplit"
-                v-model:navigation-style-type="navigationStyleType"
-                :disabled="isFullContent"
-                :disabled-navigation-split="!isMixedNav"
-              />
-            </Block>
-
-            <Block :title="$t('preferences.breadcrumb.title')">
-              <Breadcrumb
-                v-model:breadcrumb-enable="breadcrumbEnable"
-                v-model:breadcrumb-hide-only-one="breadcrumbHideOnlyOne"
-                v-model:breadcrumb-show-home="breadcrumbShowHome"
-                v-model:breadcrumb-show-icon="breadcrumbShowIcon"
-                v-model:breadcrumb-style-type="breadcrumbStyleType"
-                :disabled="
-                  !showBreadcrumbConfig || !(isSideNav || isSideMixedNav)
-                "
-              />
-            </Block>
-
-            <Block :title="$t('preferences.tabbar.title')">
-              <Tabbar
-                v-model:tabbar-enable="tabbarEnable"
-                v-model:tabbar-show-icon="tabbarShowIcon"
-              />
-            </Block>
-            <Block :title="$t('preferences.footer.title')">
-              <Footer
-                v-model:footer-enable="footerEnable"
-                v-model:footer-fixed="footerFixed"
-              />
-            </Block>
-            <Block :title="$t('preferences.copyright.title')">
-              <Copyright
-                v-model:copyright-company-name="copyrightCompanyName"
-                v-model:copyright-company-site-link="copyrightCompanySiteLink"
-                v-model:copyright-date="copyrightDate"
-                v-model:copyright-enable="copyrightEnable"
-                v-model:copyright-icp="copyrightIcp"
-                v-model:copyright-icp-link="copyrightIcpLink"
-                :disabled="!footerEnable"
-              />
-            </Block>
-          </template>
-
-          <template #shortcutKey>
-            <Block :title="$t('preferences.shortcut-keys.global')">
-              <GlobalShortcutKeys
-                v-model:shortcut-keys-enable="shortcutKeysEnable"
-                v-model:shortcut-keys-global-search="shortcutKeysGlobalSearch"
-                v-model:shortcut-keys-logout="shortcutKeysGlobalLogout"
-                v-model:shortcut-keys-preferences="
-                  shortcutKeysGlobalPreferences
-                "
-              />
-            </Block>
-          </template>
-        </VbenSegmented>
-      </div>
-
-      <template #footer>
-        <VbenButton
-          :disabled="!diffPreference"
-          class="mx-4 w-full"
-          size="sm"
-          variant="outline"
-          @click="handleClearCache"
-        >
-          <IcRoundRestartAlt class="mr-2 size-4" />
-          {{ $t('preferences.clear-and-logout') }}
-        </VbenButton>
-        <VbenButton
-          :disabled="!diffPreference"
-          class="mr-4 w-full"
-          size="sm"
-          variant="default"
-          @click="handleCopy"
-        >
-          <IcRoundFolderCopy class="mr-2 size-3" />
-          {{ $t('preferences.copy') }}
-        </VbenButton>
-      </template>
-    </VbenSheet>
-  </div>
+  <Preferences v-bind="attrs" v-on="listen" />
 </template>

+ 0 - 21
packages/business/layouts/src/widgets/preferences/trigger.vue

@@ -1,21 +0,0 @@
-<script setup lang="ts">
-import { $t } from '@vben-core/locales';
-import { VbenButton } from '@vben-core/shadcn-ui';
-
-import IconSetting from './icons/setting.vue';
-
-defineOptions({
-  name: 'PreferenceTrigger',
-});
-</script>
-
-<template>
-  <VbenButton
-    :title="$t('preferences.title')"
-    class="bg-primary flex-col-center h-12 w-12 cursor-pointer rounded-l-lg rounded-r-none border-none"
-  >
-    <IconSetting
-      class="duration-3000 fill-primary-foreground animate-spin text-2xl"
-    />
-  </VbenButton>
-</template>

+ 1 - 1
packages/business/layouts/src/widgets/user-dropdown/user-dropdown.vue

@@ -24,7 +24,7 @@ import { isWindowsOs } from '@vben-core/toolkit';
 
 import { useMagicKeys, whenever } from '@vueuse/core';
 
-import { useOpenPreferences } from '../preferences/use-open-preferences';
+import { useOpenPreferences } from '../preferences';
 
 interface Props {
   /**