Prechádzať zdrojové kódy

pref: pinia storeToRefs will be used for state management, which simplifies state access in multiple components and improves code readability and maintainability

nian 3 mesiacov pred
rodič
commit
9a98dc59bd

+ 5 - 4
src/App.vue

@@ -10,6 +10,7 @@ import {
   NGlobalStyle,
   NEl,
 } from 'naive-ui'
+import { storeToRefs } from 'pinia'
 import { provide, ref } from 'vue'
 import { RouterView } from 'vue-router'
 
@@ -21,7 +22,7 @@ import { layoutInjectionKey, mediaQueryInjectionKey } from './injection'
 
 import type { LayoutSlideDirection } from './injection'
 
-const preferencesStore = usePreferencesStore()
+const { showWatermark, showNoise, watermarkOptions } = storeToRefs(usePreferencesStore())
 const configProviderProps = getConfigProviderProps()
 
 const mediaQuery = {
@@ -58,11 +59,11 @@ provide(layoutInjectionKey, {
             <NDialogProvider>
               <RouterView />
               <NWatermark
-                v-if="preferencesStore.preferences.showWatermark"
+                v-if="showWatermark"
                 fullscreen
-                v-bind="preferencesStore.preferences.watermarkOptions"
+                v-bind="watermarkOptions"
               />
-              <Noise v-if="preferencesStore.preferences.showNoise" />
+              <Noise v-if="showNoise" />
             </NDialogProvider>
           </NMessageProvider>
         </NNotificationProvider>

+ 4 - 2
src/components/Noise.vue

@@ -1,15 +1,17 @@
 <script setup lang="ts">
+import { storeToRefs } from 'pinia'
+
 import noisePng from '@/assets/noise.png'
 import { usePreferencesStore } from '@/stores'
 
-const preferencesStore = usePreferencesStore()
+const { noiseOpacity } = storeToRefs(usePreferencesStore())
 </script>
 <template>
   <div
     class="pointer-events-none fixed inset-0 z-[99998] size-full"
     :style="{
       backgroundImage: `url(${noisePng})`,
-      opacity: preferencesStore.preferences.noiseOpacity,
+      opacity: noiseOpacity,
     }"
   />
 </template>

+ 3 - 1
src/components/UserDropdown.vue

@@ -15,6 +15,8 @@ defineOptions({
 })
 
 const userStore = useUserStore()
+const { cleanup } = userStore
+
 const message = useMessage()
 
 const userDropdownOptions = [
@@ -36,7 +38,7 @@ const onUserDropdownSelected = (key: string) => {
       message.info('点击了个人中心')
       break
     case 'signOut':
-      userStore.cleanup()
+      cleanup()
       break
   }
 }

+ 6 - 5
src/layout/aside/SidebarMenu.vue

@@ -1,6 +1,7 @@
 <script setup lang="tsx">
 import { isFunction } from 'lodash-es'
 import { NMenu, NScrollbar } from 'naive-ui'
+import { storeToRefs } from 'pinia'
 import { h, ref, useTemplateRef, watch } from 'vue'
 
 import router from '@/router'
@@ -8,9 +9,9 @@ import { usePreferencesStore, useUserStore } from '@/stores'
 
 import type { MenuInst, MenuProps } from 'naive-ui'
 
-const preferencesStore = usePreferencesStore()
+const { sidebarMenu } = storeToRefs(usePreferencesStore())
 
-const userStore = useUserStore()
+const { menuList } = storeToRefs(useUserStore())
 
 const menuRef = useTemplateRef<MenuInst>('menuRef')
 
@@ -39,11 +40,11 @@ watch(
   <NScrollbar>
     <NMenu
       ref="menuRef"
-      :collapsed-width="preferencesStore.preferences.sidebarMenu.width"
-      :collapsed="preferencesStore.preferences.sidebarMenu.collapsed"
+      :collapsed-width="sidebarMenu.width"
+      :collapsed="sidebarMenu.collapsed"
       :collapsed-icon-size="20"
       :value="menuActiveKey"
-      :options="userStore.menuList"
+      :options="menuList"
       :render-icon="renderIcon"
       :dropdown-props="{
         size: 'medium',

+ 9 - 8
src/layout/aside/SidebarUserPanel.vue

@@ -1,14 +1,15 @@
 <script setup lang="ts">
 import { useMessage } from 'naive-ui'
+import { storeToRefs } from 'pinia'
 
 import { ButtonAnimation } from '@/components'
 import Avatar from '@/components/UserAvatar.vue'
 import UserDropdown from '@/components/UserDropdown.vue'
 import { usePreferencesStore, useUserStore } from '@/stores'
 
-const preferencesStore = usePreferencesStore()
+const { sidebarMenu } = storeToRefs(usePreferencesStore())
 
-const userStore = useUserStore()
+const { user } = storeToRefs(useUserStore())
 
 const message = useMessage()
 
@@ -20,7 +21,7 @@ const handleUserCardClick = () => {
   <div
     class="flex cursor-pointer items-center transition-[background-color,border-radius,margin,padding] hover:bg-neutral-200/90 dark:hover:bg-neutral-750/65"
     :class="
-      preferencesStore.preferences.sidebarMenu.collapsed
+      sidebarMenu.collapsed
         ? 'mx-2 rounded-naive'
         : 'mx-4 rounded-xl bg-neutral-150 py-3.5 pr-2.5 pl-3.5 dark:bg-neutral-800'
     "
@@ -28,15 +29,15 @@ const handleUserCardClick = () => {
   >
     <UserDropdown
       placement="right-end"
-      :disabled="!preferencesStore.preferences.sidebarMenu.collapsed"
+      :disabled="!sidebarMenu.collapsed"
     >
       <div
         class="grid place-items-center overflow-hidden rounded-full transition-[margin,padding]"
-        :class="preferencesStore.preferences.sidebarMenu.collapsed ? 'mr-0 px-2 py-1.5' : 'mr-2'"
+        :class="sidebarMenu.collapsed ? 'mr-0 px-2 py-1.5' : 'mr-2'"
       >
         <div
           class="flex items-center justify-center overflow-hidden transition-[height,width]"
-          :class="preferencesStore.preferences.sidebarMenu.collapsed ? 'size-8' : 'size-10'"
+          :class="sidebarMenu.collapsed ? 'size-8' : 'size-10'"
         >
           <Avatar
             size="large"
@@ -57,12 +58,12 @@ const handleUserCardClick = () => {
     >
       <div
         class="grid flex-1 overflow-hidden"
-        v-show="!preferencesStore.preferences.sidebarMenu.collapsed"
+        v-show="!sidebarMenu.collapsed"
       >
         <div class="flex min-w-0 items-center overflow-hidden">
           <div class="flex flex-1 flex-col gap-y-px">
             <span class="truncate text-sm">
-              {{ userStore.user.name }}
+              {{ user.name }}
             </span>
             <span class="truncate text-xs text-neutral-450 dark:text-neutral-500">
               这里应该写点什么

+ 9 - 8
src/layout/aside/index.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { storeToRefs } from 'pinia'
 import { computed } from 'vue'
 
 import { usePreferencesStore, DEFAULT_PREFERENCES_OPTIONS } from '@/stores'
@@ -11,19 +12,19 @@ defineOptions({
 })
 
 const preferencesStore = usePreferencesStore()
+const { modify } = preferencesStore
+const { sidebarMenu } = storeToRefs(preferencesStore)
 
 const menuCollapseWidth = computed(() => {
-  return preferencesStore.preferences.sidebarMenu.collapsed
-    ? preferencesStore.preferences.sidebarMenu.width ||
-        DEFAULT_PREFERENCES_OPTIONS.sidebarMenu.width
-    : preferencesStore.preferences.sidebarMenu.maxWidth ||
-        DEFAULT_PREFERENCES_OPTIONS.sidebarMenu.maxWidth
+  return sidebarMenu.value.collapsed
+    ? sidebarMenu.value.width || DEFAULT_PREFERENCES_OPTIONS.sidebarMenu.width
+    : sidebarMenu.value.maxWidth || DEFAULT_PREFERENCES_OPTIONS.sidebarMenu.maxWidth
 })
 
 function handleCollapseClick() {
-  preferencesStore.modify({
+  modify({
     sidebarMenu: {
-      collapsed: !preferencesStore.preferences.sidebarMenu.collapsed,
+      collapsed: !sidebarMenu.value.collapsed,
     },
   })
 }
@@ -44,7 +45,7 @@ function handleCollapseClick() {
       <span
         class="iconify size-4.5 transition-[color,rotate] ph--caret-left dark:text-neutral-400"
         :class="{
-          'rotate-180': preferencesStore.preferences.sidebarMenu.collapsed,
+          'rotate-180': sidebarMenu.collapsed,
         }"
       />
     </div>

+ 34 - 23
src/layout/header/action/PreferencesDrawer.vue

@@ -8,6 +8,7 @@ import {
   useModal,
   NScrollbar,
 } from 'naive-ui'
+import { storeToRefs } from 'pinia'
 import { h, ref } from 'vue'
 
 import { ButtonAnimation, ButtonAnimationProvider } from '@/components'
@@ -26,12 +27,28 @@ const { isSmallScreen } = useInjection(mediaQueryInjectionKey)
 
 const preferencesStore = usePreferencesStore()
 
+const { modify, reset } = preferencesStore
+
+const {
+  enableNavigationTransition,
+  enableTextSelect,
+  navigationMode,
+  showTopLoadingBar,
+  showLogo,
+  showNavigation,
+  showBreadcrumb,
+  showTabs,
+  showTabClose,
+  showFooter,
+  showWatermark,
+  showNoise,
+  sidebarMenu,
+} = storeToRefs(preferencesStore)
+
 const systemStore = useSystemStore()
 
 const { color, setColor } = usePersonalization()
 
-const { modify, reset } = preferencesStore
-
 const modal = useModal()
 
 const { scrollbarInModal } = useComponentThemeOverrides()
@@ -153,12 +170,10 @@ const showNoiseModal = () => {
               <div class="flex items-center justify-between">
                 <span>展开侧边菜单</span>
                 <NSwitch
-                  :value="preferencesStore.preferences.sidebarMenu.collapsed"
+                  :value="sidebarMenu.collapsed"
                   :checked-value="false"
                   :unchecked-value="true"
-                  :disabled="
-                    isSmallScreen || preferencesStore.preferences.navigationMode !== 'sidebar'
-                  "
+                  :disabled="isSmallScreen || navigationMode !== 'sidebar'"
                   @update-value="
                     (value) =>
                       modify({
@@ -172,7 +187,7 @@ const showNoiseModal = () => {
               <div class="flex items-center justify-between">
                 <span>显示顶部加载条</span>
                 <NSwitch
-                  :value="preferencesStore.preferences.showTopLoadingBar"
+                  :value="showTopLoadingBar"
                   @update-value="
                     (value) =>
                       modify({
@@ -184,7 +199,7 @@ const showNoiseModal = () => {
               <div class="flex items-center justify-between">
                 <span>显示Logo</span>
                 <NSwitch
-                  :value="preferencesStore.preferences.showLogo"
+                  :value="showLogo"
                   @update-value="
                     (value) =>
                       modify({
@@ -196,10 +211,8 @@ const showNoiseModal = () => {
               <div class="flex items-center justify-between">
                 <span>显示导航按钮</span>
                 <NSwitch
-                  :value="preferencesStore.preferences.showNavigation"
-                  :disabled="
-                    isSmallScreen || preferencesStore.preferences.navigationMode !== 'sidebar'
-                  "
+                  :value="showNavigation"
+                  :disabled="isSmallScreen || navigationMode !== 'sidebar'"
                   @update-value="
                     (value) =>
                       modify({
@@ -211,10 +224,8 @@ const showNoiseModal = () => {
               <div class="flex items-center justify-between">
                 <span>显示面包屑</span>
                 <NSwitch
-                  :value="preferencesStore.preferences.showBreadcrumb"
-                  :disabled="
-                    isSmallScreen || preferencesStore.preferences.navigationMode !== 'sidebar'
-                  "
+                  :value="showBreadcrumb"
+                  :disabled="isSmallScreen || navigationMode !== 'sidebar'"
                   @update-value="
                     (value) =>
                       modify({
@@ -226,7 +237,7 @@ const showNoiseModal = () => {
               <div class="flex items-center justify-between">
                 <span>显示标签页</span>
                 <NSwitch
-                  :value="preferencesStore.preferences.showTabs"
+                  :value="showTabs"
                   :disabled="isSmallScreen"
                   @update-value="
                     (value) =>
@@ -239,7 +250,7 @@ const showNoiseModal = () => {
               <div class="flex items-center justify-between">
                 <span>常显标签关闭按钮</span>
                 <NSwitch
-                  :value="preferencesStore.preferences.showTabClose"
+                  :value="showTabClose"
                   :disabled="isSmallScreen"
                   @update-value="
                     (value) =>
@@ -252,7 +263,7 @@ const showNoiseModal = () => {
               <div class="flex items-center justify-between">
                 <span>显示底部</span>
                 <NSwitch
-                  :value="preferencesStore.preferences.showFooter"
+                  :value="showFooter"
                   :disabled="isSmallScreen"
                   @update-value="
                     (value) =>
@@ -278,7 +289,7 @@ const showNoiseModal = () => {
                 </ButtonAnimation>
               </div>
               <NSwitch
-                :value="preferencesStore.preferences.showWatermark"
+                :value="showWatermark"
                 @update-value="
                   (value) =>
                     modify({
@@ -300,7 +311,7 @@ const showNoiseModal = () => {
                 </ButtonAnimation>
               </div>
               <NSwitch
-                :value="preferencesStore.preferences.showNoise"
+                :value="showNoise"
                 @update-value="
                   (value) =>
                     modify({
@@ -313,7 +324,7 @@ const showNoiseModal = () => {
               <div class="flex items-center justify-between">
                 <span>启用导航过渡效果</span>
                 <NSwitch
-                  :value="preferencesStore.preferences.enableNavigationTransition"
+                  :value="enableNavigationTransition"
                   :disabled="isSmallScreen"
                   @update-value="
                     (value) =>
@@ -326,7 +337,7 @@ const showNoiseModal = () => {
               <div class="flex items-center justify-between">
                 <span>文字可选中</span>
                 <NSwitch
-                  :value="preferencesStore.preferences.enableTextSelect"
+                  :value="enableTextSelect"
                   @update-value="
                     (value) =>
                       modify({

+ 3 - 5
src/layout/header/action/SignOut.vue

@@ -6,6 +6,8 @@ import { useComponentModifier } from '@/composables'
 import { useUserStore } from '@/stores'
 
 const userStore = useUserStore()
+const { cleanup } = userStore
+
 const dialog = useDialog()
 
 const { getModalModifier } = useComponentModifier()
@@ -17,13 +19,9 @@ const handleSignOutClick = () => {
     content: '确定要退出登录吗?',
     positiveText: '确定',
     negativeText: '取消',
-    onPositiveClick: cleanupUserInfo,
+    onPositiveClick: () => cleanup(),
   })
 }
-
-function cleanupUserInfo() {
-  userStore.cleanup()
-}
 </script>
 <template>
   <ButtonAnimation @click="handleSignOutClick">

+ 8 - 4
src/layout/header/action/component/LayoutThumbnail.vue

@@ -1,10 +1,14 @@
 <script setup lang="ts">
+import { storeToRefs } from 'pinia'
+
 import { usePersonalization } from '@/composables'
 import { usePreferencesStore } from '@/stores'
 
 const { color } = usePersonalization()
 
 const preferencesStore = usePreferencesStore()
+const { modify } = preferencesStore
+const { navigationMode } = storeToRefs(preferencesStore)
 </script>
 <template>
   <div
@@ -16,11 +20,11 @@ const preferencesStore = usePreferencesStore()
     <div
       class="flex h-16 w-20 cursor-pointer flex-col rounded border border-neutral-350 outline-offset-4 transition-[outline] max-sm:pointer-events-none max-sm:opacity-50 dark:border-neutral-650"
       :class="
-        preferencesStore.preferences.navigationMode === 'sidebar'
+        navigationMode === 'sidebar'
           ? 'outline-2 outline-primary/50'
           : 'outline-2 outline-transparent hover:outline-primary/30'
       "
-      @click="preferencesStore.modify({ navigationMode: 'sidebar' })"
+      @click="modify({ navigationMode: 'sidebar' })"
     >
       <div class="flex h-2.5">
         <div class="h-full w-5 shrink-0 border-r border-neutral-350 dark:border-neutral-650"></div>
@@ -57,11 +61,11 @@ const preferencesStore = usePreferencesStore()
     <div
       class="flex h-16 w-20 cursor-pointer flex-col rounded border border-neutral-350 outline-offset-4 transition-[outline] duration-300 max-sm:pointer-events-none max-sm:opacity-50 dark:border-neutral-650"
       :class="
-        preferencesStore.preferences.navigationMode === 'horizontal'
+        navigationMode === 'horizontal'
           ? 'outline-2 outline-primary/50'
           : 'outline-2 outline-transparent hover:outline-primary/30'
       "
-      @click="preferencesStore.modify({ navigationMode: 'horizontal' })"
+      @click="modify({ navigationMode: 'horizontal' })"
     >
       <div class="flex h-2.5 border-b border-neutral-350 dark:border-neutral-650">
         <div class="h-full w-3 shrink-0"></div>

+ 6 - 9
src/layout/header/action/component/NoiseModal.vue

@@ -1,14 +1,15 @@
 <script setup lang="ts">
 import { NSlider, NInputNumber } from 'naive-ui'
-import { reactive, ref, watchEffect } from 'vue'
+import { reactive, ref } from 'vue'
 
 import { usePreferencesStore } from '@/stores'
 
 import type { SliderProps } from 'naive-ui'
 
 const preferencesStore = usePreferencesStore()
+const { modify } = preferencesStore
 
-const opacity = ref(0)
+const opacity = ref(preferencesStore.noiseOpacity)
 
 const sliderRange = reactive({
   step: 0.001,
@@ -16,15 +17,11 @@ const sliderRange = reactive({
   max: 0.1,
 })
 
-const onSliderUpdate: SliderProps['onUpdateValue'] = (opacity) => {
-  preferencesStore.modify({
-    noiseOpacity: opacity,
+const onSliderUpdate: SliderProps['onUpdateValue'] = (value) => {
+  modify({
+    noiseOpacity: value,
   })
 }
-
-watchEffect(() => {
-  opacity.value = preferencesStore.preferences.noiseOpacity
-})
 </script>
 <template>
   <div class="flex flex-col gap-y-4 pt-6 pb-4">

+ 25 - 22
src/layout/header/action/component/WatermarkModal.vue

@@ -9,6 +9,7 @@ import {
   NSwitch,
   NSelect,
 } from 'naive-ui'
+import { storeToRefs } from 'pinia'
 
 import { HintHelp } from '@/components'
 import { usePreferencesStore } from '@/stores'
@@ -16,9 +17,11 @@ import { usePreferencesStore } from '@/stores'
 import type { PreferencesOptions } from '@/stores'
 
 const preferencesStore = usePreferencesStore()
+const { modify } = preferencesStore
+const { watermarkOptions } = storeToRefs(preferencesStore)
 
 const modifyWatermarkColor = (color: string) => {
-  preferencesStore.modify({
+  modify({
     watermarkOptions: {
       fontColor: color,
     },
@@ -29,7 +32,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
   key: K,
   value: PreferencesOptions['watermarkOptions'][K],
 ) => {
-  preferencesStore.modify({
+  modify({
     watermarkOptions: {
       [key]: value,
     },
@@ -39,7 +42,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
 <template>
   <NForm
     :label-width="80"
-    :model="preferencesStore.preferences.watermarkOptions"
+    :model="watermarkOptions"
     :show-feedback="false"
     class="space-y-4"
   >
@@ -49,7 +52,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
     >
       <NInput
         type="textarea"
-        v-model:value="preferencesStore.preferences.watermarkOptions.content"
+        v-model:value="watermarkOptions.content"
         clearable
         @update:value="(value) => updateWatermarkOptions('content', value)"
       />
@@ -62,7 +65,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         class="w-full"
       >
         <NInputNumber
-          v-model:value="preferencesStore.preferences.watermarkOptions.fontSize"
+          v-model:value="watermarkOptions.fontSize"
           :min="8"
           :max="32"
           @update:value="(value) => updateWatermarkOptions('fontSize', value ?? 0)"
@@ -74,7 +77,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         class="w-full"
       >
         <NColorPicker
-          :default-value="preferencesStore.preferences.watermarkOptions.fontColor"
+          :default-value="watermarkOptions.fontColor"
           @update-value="
             (value) => {
               modifyWatermarkColor(value)
@@ -88,7 +91,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         class="w-full"
       >
         <NSelect
-          v-model:value="preferencesStore.preferences.watermarkOptions.fontStyle"
+          v-model:value="watermarkOptions.fontStyle"
           :options="[
             { label: '正常', value: 'normal' },
             { label: '斜体', value: 'italic' },
@@ -106,7 +109,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         class="w-full"
       >
         <NInputNumber
-          v-model:value="preferencesStore.preferences.watermarkOptions.lineHeight"
+          v-model:value="watermarkOptions.lineHeight"
           :min="1"
         />
       </NFormItem>
@@ -116,7 +119,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         class="w-full"
       >
         <NInputNumber
-          v-model:value="preferencesStore.preferences.watermarkOptions.fontWeight"
+          v-model:value="watermarkOptions.fontWeight"
           :min="100"
           :max="900"
           :step="100"
@@ -130,7 +133,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         path="width"
       >
         <NInputNumber
-          v-model:value="preferencesStore.preferences.watermarkOptions.width"
+          v-model:value="watermarkOptions.width"
           class="w-full"
           :min="1"
           @update:value="(value) => updateWatermarkOptions('width', value ?? 0)"
@@ -141,7 +144,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         path="height"
       >
         <NInputNumber
-          v-model:value="preferencesStore.preferences.watermarkOptions.height"
+          v-model:value="watermarkOptions.height"
           class="w-full"
           :min="1"
           @update:value="(value) => updateWatermarkOptions('height', value ?? 0)"
@@ -155,7 +158,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         path="xGap"
       >
         <NInputNumber
-          v-model:value="preferencesStore.preferences.watermarkOptions.xGap"
+          v-model:value="watermarkOptions.xGap"
           class="w-full"
           @update:value="(value) => updateWatermarkOptions('xGap', value ?? 0)"
         />
@@ -165,7 +168,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         path="yGap"
       >
         <NInputNumber
-          v-model:value="preferencesStore.preferences.watermarkOptions.yGap"
+          v-model:value="watermarkOptions.yGap"
           class="w-full"
           @update:value="(value) => updateWatermarkOptions('yGap', value ?? 0)"
         />
@@ -177,7 +180,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         path="xoffset"
       >
         <NInputNumber
-          v-model:value="preferencesStore.preferences.watermarkOptions.xOffset"
+          v-model:value="watermarkOptions.xOffset"
           class="w-full"
           @update:value="(value) => updateWatermarkOptions('xOffset', value ?? 0)"
         />
@@ -187,7 +190,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         path="yGap"
       >
         <NInputNumber
-          v-model:value="preferencesStore.preferences.watermarkOptions.yOffset"
+          v-model:value="watermarkOptions.yOffset"
           class="w-full"
           @update:value="(value) => updateWatermarkOptions('yOffset', value ?? 0)"
         />
@@ -200,7 +203,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         class="w-full"
       >
         <NSlider
-          v-model:value="preferencesStore.preferences.watermarkOptions.rotate"
+          v-model:value="watermarkOptions.rotate"
           :min="-90"
           :max="90"
           @update:value="(value) => updateWatermarkOptions('rotate', value ?? 0)"
@@ -212,7 +215,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         class="w-full"
       >
         <NSlider
-          v-model:value="preferencesStore.preferences.watermarkOptions.globalRotate"
+          v-model:value="watermarkOptions.globalRotate"
           :min="-180"
           :max="180"
           @update:value="(value) => updateWatermarkOptions('globalRotate', value ?? 0)"
@@ -224,7 +227,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
         class="w-full"
       >
         <NSwitch
-          v-model:value="preferencesStore.preferences.watermarkOptions.cross"
+          v-model:value="watermarkOptions.cross"
           @update:value="(value) => updateWatermarkOptions('cross', value)"
         />
       </NFormItem>
@@ -235,7 +238,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
     >
       <NInput
         type="textarea"
-        v-model:value="preferencesStore.preferences.watermarkOptions.image"
+        v-model:value="watermarkOptions.image"
         @update:value="(value) => updateWatermarkOptions('image', value)"
         clearable
       />
@@ -254,7 +257,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
             class="pb-1.5"
           />
           <NInputNumber
-            v-model:value="preferencesStore.preferences.watermarkOptions.imageWidth"
+            v-model:value="watermarkOptions.imageWidth"
             @update:value="(value) => updateWatermarkOptions('imageWidth', value ?? 0)"
           />
         </div>
@@ -271,7 +274,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
             class="pb-1.5"
           />
           <NInputNumber
-            v-model:value="preferencesStore.preferences.watermarkOptions.imageHeight"
+            v-model:value="watermarkOptions.imageHeight"
             @update:value="(value) => updateWatermarkOptions('imageHeight', value ?? 0)"
           />
         </div>
@@ -289,7 +292,7 @@ const updateWatermarkOptions = <K extends keyof PreferencesOptions['watermarkOpt
             class="pb-1.5"
           />
           <NSlider
-            v-model:value="preferencesStore.preferences.watermarkOptions.imageOpacity"
+            v-model:value="watermarkOptions.imageOpacity"
             :min="0"
             :max="1"
             :step="0.01"

+ 3 - 5
src/layout/header/action/index.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { storeToRefs } from 'pinia'
 import { defineAsyncComponent, h } from 'vue'
 
 import { ButtonAnimation } from '@/components'
@@ -21,8 +22,7 @@ const AsyncAvatarDropdown = defineAsyncComponent({
 })
 
 const { isSmallScreen } = useInjection(mediaQueryInjectionKey)
-
-const preferencesStore = usePreferencesStore()
+const { navigationMode } = storeToRefs(usePreferencesStore())
 </script>
 <template>
   <div class="flex items-center">
@@ -38,8 +38,6 @@ const preferencesStore = usePreferencesStore()
     <ThemePopselect />
     <PreferencesDrawer />
     <SignOut />
-    <AsyncAvatarDropdown
-      v-if="!isSmallScreen && preferencesStore.preferences.navigationMode === 'horizontal'"
-    />
+    <AsyncAvatarDropdown v-if="!isSmallScreen && navigationMode === 'horizontal'" />
   </div>
 </template>

+ 4 - 6
src/layout/header/index.vue

@@ -1,4 +1,6 @@
 <script setup lang="ts">
+import { storeToRefs } from 'pinia'
+
 import { usePreferencesStore } from '@/stores'
 
 import Action from './action/index.vue'
@@ -9,18 +11,14 @@ defineOptions({
   name: 'HeaderLayout',
 })
 
-const preferencesStore = usePreferencesStore()
+const { navigationMode } = storeToRefs(usePreferencesStore())
 </script>
 <template>
   <header class="flex bg-naive-card transition-[background-color]">
     <Logo />
     <div
       class="flex flex-1 items-center border-l px-4 py-3.5 transition-[border-color]"
-      :class="
-        preferencesStore.preferences.navigationMode === 'sidebar'
-          ? 'border-naive-border'
-          : 'border-transparent'
-      "
+      :class="navigationMode === 'sidebar' ? 'border-naive-border' : 'border-transparent'"
     >
       <Navigation />
       <Action class="gap-x-3 pl-4" />

+ 8 - 12
src/layout/header/logo/index.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { storeToRefs } from 'pinia'
 import { nextTick, ref, watch } from 'vue'
 
 import Logo from '@/components/AppLogo.vue'
@@ -10,24 +11,21 @@ defineOptions({
 
 const APP_NAME = import.meta.env.VITE_APP_NAME
 
-const preferencesStore = usePreferencesStore()
+const { navigationMode, sidebarMenu, showLogo } = storeToRefs(usePreferencesStore())
 
 const logoAreaWrapRef = ref<HTMLElement | null>(null)
 
 const collapseWidth = ref(0)
 
 watch(
-  [
-    () => preferencesStore.preferences.navigationMode,
-    () => preferencesStore.preferences.sidebarMenu.collapsed,
-  ],
+  [() => navigationMode.value, () => sidebarMenu.value.collapsed],
   ([navigationMode, isCollapsed]) => {
     if (navigationMode === 'horizontal') {
       nextTick(() => {
         collapseWidth.value = logoAreaWrapRef.value?.clientWidth ?? 0
       })
     } else {
-      const { width, maxWidth } = preferencesStore.preferences.sidebarMenu
+      const { width, maxWidth } = sidebarMenu.value
       const { width: defaultWidth, maxWidth: defaultMaxWidth } =
         DEFAULT_PREFERENCES_OPTIONS.sidebarMenu
       collapseWidth.value = isCollapsed ? width || defaultWidth : maxWidth || defaultMaxWidth
@@ -51,10 +49,10 @@ watch(
       ref="logoAreaWrapRef"
       class="flex h-full items-center transition-[opacity,padding]"
       :class="[
-        preferencesStore.preferences.sidebarMenu.collapsed ? 'px-3' : 'px-4',
+        sidebarMenu.collapsed ? 'px-3' : 'px-4',
         {
-          'opacity-0': !preferencesStore.preferences.showLogo,
-          'w-fit': preferencesStore.preferences.navigationMode === 'horizontal',
+          'opacity-0': !showLogo,
+          'w-fit': navigationMode === 'horizontal',
         },
       ]"
     >
@@ -63,9 +61,7 @@ watch(
       </div>
       <div
         class="flex flex-1 overflow-hidden transition-[margin-left,max-width]"
-        :class="
-          preferencesStore.preferences.sidebarMenu.collapsed ? 'ml-0 max-w-0' : 'ml-4 max-w-44'
-        "
+        :class="sidebarMenu.collapsed ? 'ml-0 max-w-0' : 'ml-4 max-w-44'"
       >
         <h1 class="shrink-0 text-xl">
           {{ APP_NAME }}

+ 4 - 3
src/layout/header/navigation/HorizontalMenu.vue

@@ -2,6 +2,7 @@
 import { useElementSize, watchThrottled } from '@vueuse/core'
 import { isFunction, isEmpty } from 'lodash-es'
 import { NDropdown } from 'naive-ui'
+import { storeToRefs } from 'pinia'
 import { h, computed, ref, watch, nextTick, onBeforeUnmount, reactive } from 'vue'
 
 import { useInjection } from '@/composables'
@@ -24,7 +25,7 @@ let rafId: number | null = null
 
 let initialized = false
 
-const userStore = useUserStore()
+const { menuList } = storeToRefs(useUserStore())
 
 const { navigationContainerElement } = useInjection(headerLayoutInjectionKey)
 
@@ -41,7 +42,7 @@ const menuRightBoundMap = reactive(new Map<Key, number>())
 const threshold = ref(Number.POSITIVE_INFINITY)
 
 const moreDropdownOptions = computed<DropdownProps['options']>(() => {
-  return (userStore.menuList as NonNullable<MenuProps['options']>).filter((item) => {
+  return (menuList.value as NonNullable<MenuProps['options']>).filter((item) => {
     if (item.type) return false
     const menuRightBound = menuRightBoundMap.get(item.key) ?? 0
     return menuRightBound + MENU.ITEM_COLUMN_GAP > threshold.value
@@ -149,7 +150,7 @@ onBeforeUnmount(() => {
     }"
   >
     <template
-      v-for="{ disabled, key, type, label, icon, children } in userStore.menuList"
+      v-for="{ disabled, key, type, label, icon, children } in menuList"
       :key="key"
     >
       <div

+ 5 - 14
src/layout/header/navigation/index.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+import { storeToRefs } from 'pinia'
 import { defineAsyncComponent, provide, ref } from 'vue'
 
 import { CollapseTransition } from '@/components'
@@ -13,7 +14,7 @@ const AsyncNavigationButton = defineAsyncComponent(() => import('./NavigationBut
 const AsyncHorizontalMenu = defineAsyncComponent(() => import('./HorizontalMenu.vue'))
 const AsyncBreadcrumb = defineAsyncComponent(() => import('./Breadcrumb.vue'))
 
-const preferencesStore = usePreferencesStore()
+const { showNavigation, showBreadcrumb, navigationMode } = storeToRefs(usePreferencesStore())
 
 const navigationContainerRef = ref<HTMLElement | null>(null)
 
@@ -26,23 +27,13 @@ provide(headerLayoutInjectionKey, {
     ref="navigationContainerRef"
     class="flex h-9 flex-1 items-center"
   >
-    <CollapseTransition
-      :display="
-        preferencesStore.preferences.showNavigation &&
-        preferencesStore.preferences.navigationMode === 'sidebar'
-      "
-    >
+    <CollapseTransition :display="showNavigation && navigationMode === 'sidebar'">
       <AsyncNavigationButton />
     </CollapseTransition>
-    <CollapseTransition
-      :display="
-        preferencesStore.preferences.showBreadcrumb &&
-        preferencesStore.preferences.navigationMode === 'sidebar'
-      "
-    >
+    <CollapseTransition :display="showBreadcrumb && navigationMode === 'sidebar'">
       <AsyncBreadcrumb />
     </CollapseTransition>
-    <CollapseTransition :display="preferencesStore.preferences.navigationMode === 'horizontal'">
+    <CollapseTransition :display="navigationMode === 'horizontal'">
       <AsyncHorizontalMenu />
     </CollapseTransition>
   </nav>

+ 12 - 10
src/layout/index.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import { isEmpty } from 'lodash-es'
+import { storeToRefs } from 'pinia'
 import { computed, defineAsyncComponent, h, watch } from 'vue'
 
 import texturePng from '@/assets/texture.png'
@@ -18,6 +19,8 @@ defineOptions({
 })
 
 const preferencesStore = usePreferencesStore()
+const { modify } = preferencesStore
+const { sidebarMenu, navigationMode, showFooter, showTabs } = storeToRefs(preferencesStore)
 
 const AsyncMobileHeader = defineAsyncComponent(() => import('./mobile/MobileHeader.vue'))
 const AsyncMobileLeftAside = defineAsyncComponent(() => import('./mobile/MobileLeftAside.vue'))
@@ -28,9 +31,7 @@ const AsyncAsideLayout = defineAsyncComponent({
     h('div', {
       style: {
         width: `${
-          preferencesStore.preferences.sidebarMenu.collapsed
-            ? preferencesStore.preferences.sidebarMenu.width
-            : preferencesStore.preferences.sidebarMenu.maxWidth
+          sidebarMenu.value.collapsed ? sidebarMenu.value.width : sidebarMenu.value.maxWidth
         }px`,
       },
     }),
@@ -38,6 +39,7 @@ const AsyncAsideLayout = defineAsyncComponent({
 })
 
 const tabsStore = useTabsStore()
+const { tabs } = storeToRefs(tabsStore)
 
 const { isSmallScreen } = useInjection(mediaQueryInjectionKey)
 
@@ -45,15 +47,15 @@ const { layoutSlideDirection, setLayoutSlideDirection } = useInjection(layoutInj
 
 const layoutTranslateOffset = computed(() => {
   return layoutSlideDirection.value === 'right'
-    ? preferencesStore.preferences.sidebarMenu.maxWidth || 0
+    ? sidebarMenu.value.maxWidth || 0
     : layoutSlideDirection.value === 'left'
-      ? -(preferencesStore.preferences.sidebarMenu.width || 0)
+      ? -(sidebarMenu.value.width || 0)
       : 0
 })
 
 watch(isSmallScreen, (isSmallScreen) => {
   if (isSmallScreen) {
-    preferencesStore.modify({
+    modify({
       sidebarMenu: {
         collapsed: false,
       },
@@ -88,7 +90,7 @@ watch(isSmallScreen, (isSmallScreen) => {
       <div class="flex flex-1 overflow-hidden">
         <CollapseTransition
           v-if="!isSmallScreen"
-          :display="preferencesStore.preferences.navigationMode === 'sidebar'"
+          :display="navigationMode === 'sidebar'"
           content-class="min-h-0"
         >
           <AsyncAsideLayout />
@@ -98,7 +100,7 @@ watch(isSmallScreen, (isSmallScreen) => {
         >
           <CollapseTransition
             v-if="!isSmallScreen"
-            :display="!isEmpty(tabsStore.tabs) && preferencesStore.preferences.showTabs"
+            :display="!isEmpty(tabs) && showTabs"
             direction="horizontal"
             :render-content="false"
           >
@@ -108,7 +110,7 @@ watch(isSmallScreen, (isSmallScreen) => {
             <MainLayout />
           </main>
           <EmptyPlaceholder
-            :show="isEmpty(tabsStore.tabs)"
+            :show="isEmpty(tabs)"
             description="空标签页"
             size="huge"
           >
@@ -120,7 +122,7 @@ watch(isSmallScreen, (isSmallScreen) => {
           </EmptyPlaceholder>
           <CollapseTransition
             v-if="!isSmallScreen"
-            :display="preferencesStore.preferences.showFooter"
+            :display="showFooter"
             direction="horizontal"
             :render-content="false"
           >

+ 5 - 4
src/layout/main/index.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import { isEmpty } from 'lodash-es'
+import { storeToRefs } from 'pinia'
 import { computed, nextTick, onMounted, ref, watch } from 'vue'
 import { RouterView } from 'vue-router'
 
@@ -22,7 +23,7 @@ const { shouldRefreshRoute, layoutSlideDirection, setLayoutSlideDirection } =
 
 const tabsStore = useTabsStore()
 
-const preferencesStore = usePreferencesStore()
+const { enableNavigationTransition, showTabs } = storeToRefs(usePreferencesStore())
 
 const { createTab, setTabActivePath } = tabsStore
 
@@ -85,9 +86,9 @@ watch(
       return
     }
 
-    if (!preferencesStore.preferences.enableNavigationTransition) return
+    if (!enableNavigationTransition.value) return
 
-    if (!preferencesStore.preferences.showTabs) {
+    if (!showTabs.value) {
       navigationTransitionName.value = 'scale'
       return
     }
@@ -155,7 +156,7 @@ onMounted(() => {
 </script>
 <template>
   <RouterView
-    v-if="preferencesStore.preferences.enableNavigationTransition"
+    v-if="enableNavigationTransition"
     v-slot="{ Component, route }"
   >
     <Transition

+ 31 - 33
src/layout/tabs/index.vue

@@ -1,6 +1,7 @@
 <script setup lang="tsx">
 import { isEmpty } from 'lodash-es'
 import { NDropdown, NEllipsis, NScrollbar } from 'naive-ui'
+import { storeToRefs } from 'pinia'
 import {
   computed,
   defineComponent,
@@ -47,7 +48,9 @@ const scrollbarRef = useTemplateRef<InstanceType<typeof NScrollbar>>('scrollbarR
 
 const tabsStore = useTabsStore()
 
-const preferencesStore = usePreferencesStore()
+const { tabs, tabActivePath } = storeToRefs(tabsStore)
+
+const { showTabClose } = storeToRefs(usePreferencesStore())
 
 const {
   setTabActivePath,
@@ -62,17 +65,17 @@ const {
 } = tabsStore
 
 const tabPinnedList = computed({
-  get: () => tabsStore.tabs.filter((tab) => tab.pinned),
+  get: () => tabs.value.filter((tab) => tab.pinned),
   set: (newPinnedTabs: Tab[]) => {
-    const currentUnpinnedTabs = tabsStore.tabs.filter((tab) => !tab.pinned)
+    const currentUnpinnedTabs = tabs.value.filter((tab) => !tab.pinned)
     setTabs([...newPinnedTabs, ...currentUnpinnedTabs])
   },
 })
 
 const tabUnPinnedList = computed({
-  get: () => tabsStore.tabs.filter((tab) => !tab.pinned),
+  get: () => tabs.value.filter((tab) => !tab.pinned),
   set: (newUnpinnedTabs: Tab[]) => {
-    const currentPinnedTabs = tabsStore.tabs.filter((tab) => tab.pinned)
+    const currentPinnedTabs = tabs.value.filter((tab) => tab.pinned)
     setTabs([...currentPinnedTabs, ...newUnpinnedTabs])
   },
 })
@@ -284,7 +287,7 @@ function handleTabRefreshClick() {
 
 const routerAfterEach = router.afterEach(() => {
   nextTick(() => {
-    pendingActivePath.value = tabsStore.tabActivePath
+    pendingActivePath.value = tabActivePath.value
   })
 })
 
@@ -328,26 +331,36 @@ const InternalTabs = defineComponent({
                 'relative cursor-pointer overflow-hidden border-r border-r-naive-border transition-[background-color,border-color,max-width] hover:bg-primary/6 [&:not(.max-w-0)]:max-w-48',
                 {
                   'tab-active': tab.path === pendingActivePath.value,
-                  group: !tab.locked && !preferencesStore.preferences.showTabClose,
+                  group: !tab.locked && !showTabClose,
                 },
               ]}
               onClick={() => handleTabClick(tab.path)}
               onContextmenu={(e) => handleTabContextMenuClick(e, tab)}
             >
+              <Transition
+                type='transition'
+                leaveActiveClass='transition-[opacity,scale,translate] will-change-[opacity,transform,scale]'
+                enterActiveClass='transition-[opacity,scale,translate] will-change-[opacity,transform,scale]'
+                leaveToClass={tabBackgroundTransitionClasses.leaveToClass}
+                enterFromClass={tabBackgroundTransitionClasses.enterFromClass}
+                onAfterEnter={() => {
+                  scrollToActiveTab('smooth')
+                }}
+              >
+                {tab.path === pendingActivePath.value && (
+                  <div class='absolute inset-0 size-full border-t-[1.5px] border-primary bg-primary/6' />
+                )}
+              </Transition>
               <div
-                class={[
-                  'relative z-10 flex h-full items-center pl-4',
-                  tab.pinned ? 'pr-4' : 'pr-2.5',
-                ]}
+                class={['relative flex h-full items-center pl-4', tab.pinned ? 'pr-4' : 'pr-2.5']}
               >
                 <div
                   class={[
                     'flex flex-1 items-center overflow-hidden transition-[translate]',
                     {
-                      'translate-x-2.5':
-                        !tab.pinned && (tab.locked || !preferencesStore.preferences.showTabClose),
+                      'translate-x-2.5': !tab.pinned && (tab.locked || !showTabClose.value),
                       'group-hover:translate-x-0':
-                        !tab.pinned && !tab.locked && !preferencesStore.preferences.showTabClose,
+                        !tab.pinned && !tab.locked && !showTabClose.value,
                     },
                   ]}
                 >
@@ -369,10 +382,9 @@ const InternalTabs = defineComponent({
                     class={[
                       'ml-1 flex overflow-hidden rounded-full p-1 transition-[background-color,opacity,scale] hover:bg-naive-button-hover',
                       {
-                        'scale-0 opacity-0':
-                          tab.locked || !preferencesStore.preferences.showTabClose,
+                        'scale-0 opacity-0': tab.locked || !showTabClose.value,
                         'group-hover:scale-100 group-hover:opacity-100':
-                          !tab.locked && !preferencesStore.preferences.showTabClose,
+                          !tab.locked && !showTabClose.value,
                       },
                     ]}
                     onClick={(e) => {
@@ -384,20 +396,6 @@ const InternalTabs = defineComponent({
                   </div>
                 )}
               </div>
-              <Transition
-                type='transition'
-                leaveActiveClass='transition-[opacity,scale,translate] will-change-[opacity,transform,scale]'
-                enterActiveClass='transition-[opacity,scale,translate] will-change-[opacity,transform,scale]'
-                leaveToClass={tabBackgroundTransitionClasses.leaveToClass}
-                enterFromClass={tabBackgroundTransitionClasses.enterFromClass}
-                onAfterEnter={() => {
-                  scrollToActiveTab('smooth')
-                }}
-              >
-                {tab.path === pendingActivePath.value && (
-                  <div class='absolute inset-0 size-full border-t-[1.5px] border-primary bg-primary/6' />
-                )}
-              </Transition>
             </div>
           ))}
         </TransitionGroup>
@@ -407,7 +405,7 @@ const InternalTabs = defineComponent({
 })
 
 watch(
-  [() => tabsStore.tabs, () => tabsStore.tabActivePath],
+  [() => tabs.value, () => tabActivePath.value],
   ([newTabs, newTabActivePath], [oldTabs, oldTabActivePath]) => {
     if (!newTabActivePath) {
       tabBackgroundTransitionClasses.leaveToClass = 'scale-0 opacity-0'
@@ -434,7 +432,7 @@ watch(
 
 onMounted(() => {
   scrollToActiveTab()
-  pendingActivePath.value = tabsStore.tabActivePath
+  pendingActivePath.value = tabActivePath.value
   tabBackgroundTransitionClasses.enterFromClass = 'scale-0 opacity-0'
 })
 

+ 14 - 10
src/router/guard.ts

@@ -1,3 +1,5 @@
+import { storeToRefs } from 'pinia'
+
 import { useDiscreteApi } from '@/composables'
 import { usePreferencesStore, pinia, useUserStore } from '@/stores'
 
@@ -7,18 +9,20 @@ const Layout = () => import('@/layout/index.vue')
 
 const { loadingBar } = useDiscreteApi()
 
-const preferencesStore = usePreferencesStore(pinia)
+const { showTopLoadingBar } = storeToRefs(usePreferencesStore(pinia))
 
 export function setupRouterGuard(router: Router) {
   router.beforeEach(async (to, from, next) => {
-    if (preferencesStore.preferences.showTopLoadingBar) {
+    if (showTopLoadingBar.value) {
       loadingBar.start()
     }
 
     const userStore = useUserStore()
+    const { resolveMenuList, cleanup } = userStore
+    const { token, routeList } = storeToRefs(userStore)
 
     if (to.name === 'signIn') {
-      if (!userStore.token) {
+      if (!token.value) {
         next()
       } else {
         next(from.fullPath)
@@ -27,15 +31,15 @@ export function setupRouterGuard(router: Router) {
       return false
     }
 
-    if (!userStore.token) {
-      userStore.cleanup()
+    if (!token.value) {
+      cleanup()
       next()
       return false
     }
 
-    if (userStore.token && !router.hasRoute('layout')) {
+    if (token.value && !router.hasRoute('layout')) {
       try {
-        await userStore.resolveMenuList()
+        await resolveMenuList()
 
         router.addRoute({
           path: '/',
@@ -43,13 +47,13 @@ export function setupRouterGuard(router: Router) {
           component: Layout,
           // if you need to have a redirect when accessing / routing
           redirect: '/dashboard',
-          children: userStore.routeList,
+          children: routeList.value,
         })
 
         next(to.fullPath)
       } catch (error) {
         console.error('Error resolving user menu or adding route:', error)
-        userStore.cleanup()
+        cleanup()
         next()
       }
 
@@ -65,7 +69,7 @@ export function setupRouterGuard(router: Router) {
   })
 
   router.afterEach(() => {
-    if (preferencesStore.preferences.showTopLoadingBar) {
+    if (showTopLoadingBar.value) {
       loadingBar.finish()
     }
   })

+ 9 - 1
src/stores/preferences.ts

@@ -1,7 +1,7 @@
 import { useStorage } from '@vueuse/core'
 import { mergeWith } from 'lodash-es'
 import { acceptHMRUpdate, defineStore } from 'pinia'
-import { watch } from 'vue'
+import { computed, watch, type ComputedRef } from 'vue'
 
 import type { WatermarkProps } from 'naive-ui'
 
@@ -75,6 +75,13 @@ export const DEFAULT_PREFERENCES_OPTIONS = {
 export const usePreferencesStore = defineStore('preferencesStore', () => {
   const preferences = useStorage<PreferencesOptions>('preferences', DEFAULT_PREFERENCES_OPTIONS)
 
+  const computedPreferences = Object.fromEntries(
+    Object.entries(preferences.value).map(([key]) => [
+      key,
+      computed(() => preferences.value[key as keyof PreferencesOptions]),
+    ]),
+  ) as { [K in keyof PreferencesOptions]: ComputedRef<PreferencesOptions[K]> }
+
   const modify = (options: Partial<PreferencesOptions>) => {
     preferences.value = mergeWith({}, preferences.value, options, (objValue, srcValue) => {
       if (Array.isArray(objValue) && Array.isArray(srcValue)) {
@@ -99,6 +106,7 @@ export const usePreferencesStore = defineStore('preferencesStore', () => {
 
   return {
     preferences,
+    ...computedPreferences,
     reset,
     modify,
   }

+ 1 - 3
src/views/about/index.vue

@@ -23,8 +23,7 @@ const directoryStructureHighlight = ref('')
 const dependenciesCodeHighlight = ref('')
 const devDependenciesCodeHighlight = ref('')
 
-const dir = `. 📂 lithe-admin
-├── 📄 LICENSE
+const dir = ` 📂 lithe-admin
 ├── 📄 README.en_US.md
 ├── 📄 README.md
 ├── 📄 eslint.config.ts
@@ -175,7 +174,6 @@ const dir = `. 📂 lithe-admin
 ├── 📄 tsconfig.json
 ├── 📄 tsconfig.node.json
 ├── 📄 tsconfig.vitest.json
-├── 📄 vercel.json
 ├── 📄 vite.config.ts
 └── 📄 vitest.config.ts`
 

+ 13 - 18
src/views/dashboard/index.vue

@@ -2,6 +2,7 @@
 import chroma from 'chroma-js'
 import * as echarts from 'echarts'
 import { NNumberAnimation } from 'naive-ui'
+import { storeToRefs } from 'pinia'
 import { onMounted, watch, ref, computed, onUnmounted, nextTick } from 'vue'
 
 import { ContentWrapper } from '@/components'
@@ -16,7 +17,7 @@ defineOptions({
 })
 
 const { isDark, color } = usePersonalization()
-const preferencesStore = usePreferencesStore()
+const { sidebarMenu, navigationMode } = storeToRefs(usePreferencesStore())
 
 const cardList = ref(generateCardData())
 
@@ -1050,23 +1051,17 @@ function resizeAllCharts() {
   if (highestRevenueChartInstance) highestRevenueChartInstance.resize()
 }
 
-watch(
-  [
-    () => preferencesStore.preferences.sidebarMenu.collapsed,
-    () => preferencesStore.preferences.navigationMode,
-  ],
-  () => {
-    if (collapseResizeTimeout !== null) {
-      clearTimeout(collapseResizeTimeout)
-      collapseResizeTimeout = null
-    }
-    nextTick(() => {
-      collapseResizeTimeout = setTimeout(() => {
-        resizeAllCharts()
-      }, 350)
-    })
-  },
-)
+watch([() => sidebarMenu.value.collapsed, () => navigationMode.value], () => {
+  if (collapseResizeTimeout !== null) {
+    clearTimeout(collapseResizeTimeout)
+    collapseResizeTimeout = null
+  }
+  nextTick(() => {
+    collapseResizeTimeout = setTimeout(() => {
+      resizeAllCharts()
+    }, 350)
+  })
+})
 
 watch([isDark, color], () => {
   if (revenueChartInstance) {

+ 3 - 2
src/views/sign-in/index.vue

@@ -29,6 +29,7 @@ const { isDark } = usePersonalization()
 const { isSmallScreen } = useInjection(mediaQueryInjectionKey)
 
 const userStore = useUserStore()
+const { setToken } = userStore
 
 const illustrations = [
   defineAsyncComponent(() => import('./component/Illustration1.vue')),
@@ -71,9 +72,9 @@ function toLayout() {
 
   setTimeout(() => {
     if (signInForm.account.includes('admin')) {
-      userStore.setToken('admin')
+      setToken('admin')
     } else {
-      userStore.setToken('user')
+      setToken('user')
     }
 
     router.replace({