Ver Fonte

feat: update the sidebar component, replace the menu and user card with the sidebar menu and user panel, and adjust the related styles and logic

nian há 1 mês atrás
pai
commit
d7f901e76c

+ 82 - 0
src/components/Avatar.vue

@@ -0,0 +1,82 @@
+<script setup lang="ts">
+import { NAvatar } from 'naive-ui'
+
+import type { AvatarProps as AvatarPropsRaw } from 'naive-ui'
+
+interface AvatarProps extends /** @vue-ignore */ AvatarPropsRaw {}
+
+defineProps<AvatarProps>()
+</script>
+<template>
+  <NAvatar
+    round
+    src="src path"
+    v-bind="$attrs"
+  >
+    <template #fallback>
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        viewBox="0 0 640 640"
+        class="text-primary"
+      >
+        <defs>
+          <clipPath id="a">
+            <circle
+              cx="320"
+              cy="320"
+              r="320"
+              fill="none"
+              stroke="#707070"
+              transform="translate(626 151)"
+            />
+          </clipPath>
+        </defs>
+        <g transform="translate(-626 -151)">
+          <circle
+            cx="320"
+            cy="320"
+            r="320"
+            fill="#fff"
+            transform="translate(626 151)"
+          />
+          <g clip-path="url(#a)">
+            <path
+              fill="#ffb9b9"
+              d="M900.5 416s13.9 104.4-7.4 116.1 126.8 12.8 126.8 12.8-33-90.6-18.1-118.3Z"
+            />
+            <circle
+              cx="73.5"
+              cy="73.5"
+              r="73.5"
+              fill="#ffb9b9"
+              transform="translate(881.9 324.4)"
+            />
+            <path
+              fill="currentColor"
+              d="m1128 545.4-12.3 56.1-26 118.7-.8 6.5-9 75.3-6 50-4 33.5c-24.3 12.6-42 21.2-42 21.2s-1.5-7.6-4-17.3a372.9 372.9 0 0 1-75.6 15.3c9.8 10.9-141.1-266.1-146.9-303.2a3614.4 3614.4 0 0 1-9-61.4l112.4-49.7c8 18.6 45.3 24.1 45.3 24.1 30.9-2 55.1-18.4 55.1-18.4Z"
+            />
+            <path
+              fill="#090814"
+              d="m1017.8 328.4 13.7-5.5s-28.7-31.7-68.7-29l11.2-12.3s-27.5-11-52.5 17.9c-13 15.2-28.3 33-37.8 53.1H869l6.1 13.6-21.5 13.5 22.1-2.4a75.6 75.6 0 0 0-.6 22.4 29 29 0 0 0 10.6 19s17.1-35.3 17.1-40.8v13.8s13.7-12.4 13.7-20.7l7.5 9.7 3.8-15.2 46.2 15.2-7.5-12.4 28.8 4.1-11.3-15.1s32.5 17.9 33.8 33 10.8 29.5 10.8 29.5 25.4-70.8-10.8-91.4Z"
+            />
+            <path
+              fill="currentColor"
+              d="M1208 803.3c-7.5 8.2-26 20.4-48.5 33.6a2319.9 2319.9 0 0 1-89.5 48.6c-24.4 12.6-42.1 21.2-42.1 21.2s-1.5-7.6-4-17.3c-3.3-13-8.2-30-13.3-37.4l-.6-.8c-1.5-2-3-3-4.5-3L1080 802l32.2-20-23.2-55.3-29-69.4 17.5-55.8 17.6-56h33s11 23.8 25 57.1l6.5 15.5c28.7 70 64.6 167.8 48.5 185.2ZM875.5 851.3a47.8 47.8 0 0 0-8.1.7c-23 4-29.8 24.7-31.7 38.7a71.8 71.8 0 0 0-.7 12.8L814.7 888l-7.4-5.7a134.9 134.9 0 0 1-47.5-30.2 238.5 238.5 0 0 1-32-38 334.1 334.1 0 0 1-24.6-42.3 31.2 31.2 0 0 1-.5-26.5l25.2-56 36.7-82q.4-2.9 1-5.7c7.2-41.8 26.8-60.3 26.8-60.3h14.9l10 60.3 12.4 75L821 705l-20.1 65.4 19.2 20.9Z"
+            />
+            <path
+              fill="#090814"
+              d="M1147.2 629.7H980.8v-3.4h-75.5v3.4H738.2a11.3 11.3 0 0 0-11.3 11.3v228a11.3 11.3 0 0 0 11.3 11.2h409a11.3 11.3 0 0 0 11.3-11.3V641a11.3 11.3 0 0 0-11.3-11.3Z"
+            />
+            <circle
+              cx="25"
+              cy="25"
+              r="25"
+              fill="#fff"
+              transform="translate(921.4 684.3)"
+            />
+          </g>
+        </g>
+      </svg>
+    </template>
+  </NAvatar>
+</template>

+ 54 - 0
src/components/UserDropdown.vue

@@ -0,0 +1,54 @@
+<script setup lang="ts">
+import { useMessage, NDropdown } from 'naive-ui'
+import { h } from 'vue'
+
+import { useUserStore } from '@/stores'
+
+import type { DropdownProps } from 'naive-ui'
+
+interface UserDropdownProps extends /** @vue-ignore */ DropdownProps {}
+
+defineProps<UserDropdownProps>()
+
+defineOptions({
+  inheritAttrs: false,
+})
+
+const userStore = useUserStore()
+const message = useMessage()
+
+const userDropdownOptions = [
+  {
+    icon: () => h('span', { class: 'iconify ph--user size-5' }),
+    key: 'user',
+    label: '个人中心',
+  },
+  {
+    icon: () => h('span', { class: 'iconify ph--sign-out size-5' }),
+    key: 'signOut',
+    label: '退出登录',
+  },
+]
+
+const onUserDropdownSelected = (key: string) => {
+  switch (key) {
+    case 'user':
+      message.info('点击了个人中心')
+      break
+    case 'signOut':
+      userStore.cleanup()
+      break
+  }
+}
+</script>
+<template>
+  <NDropdown
+    trigger="click"
+    :options="userDropdownOptions"
+    show-arrow
+    @select="onUserDropdownSelected"
+    v-bind="$attrs"
+  >
+    <slot />
+  </NDropdown>
+</template>

+ 3 - 3
src/components/collapse-transition/CollapseTransition.vue

@@ -29,13 +29,13 @@ const {
 
 const DIRECTION_CLASSES_MAP = {
   vertical: {
-    activeClass: 'transition-[grid-template-columns]',
+    activeClass: 'transition-[grid-template-columns] overflow-hidden',
     fromClass: 'grid-cols-[0fr]',
     toClass: 'grid-cols-[1fr]',
     contentClass: 'min-w-0',
   },
   horizontal: {
-    activeClass: 'transition-[grid-template-rows]',
+    activeClass: 'transition-[grid-template-rows] overflow-hidden',
     fromClass: 'grid-rows-[0fr]',
     toClass: 'grid-rows-[1fr]',
     contentClass: 'min-h-0',
@@ -56,7 +56,7 @@ const DIRECTION_CLASSES_MAP = {
     <div
       v-if="displayDirective === 'if' ? display : true"
       v-show="displayDirective === 'show' ? display : true"
-      class="grid overflow-hidden"
+      class="grid"
       :class="containerClass"
       :style="[
         typeof duration === 'number' &&

+ 0 - 64
src/layout/aside/component/Avatar.vue

@@ -1,64 +0,0 @@
-<template>
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 640 640"
-  >
-    <defs>
-      <clipPath id="a">
-        <circle
-          cx="320"
-          cy="320"
-          r="320"
-          fill="none"
-          stroke="#707070"
-          transform="translate(626 151)"
-        />
-      </clipPath>
-    </defs>
-    <g transform="translate(-626 -151)">
-      <circle
-        cx="320"
-        cy="320"
-        r="320"
-        fill="#fff"
-        transform="translate(626 151)"
-      />
-      <g clip-path="url(#a)">
-        <path
-          fill="#ffb9b9"
-          d="M900.5 416s13.9 104.4-7.4 116.1 126.8 12.8 126.8 12.8-33-90.6-18.1-118.3Z"
-        />
-        <circle
-          cx="73.5"
-          cy="73.5"
-          r="73.5"
-          fill="#ffb9b9"
-          transform="translate(881.9 324.4)"
-        />
-        <path
-          fill="currentColor"
-          d="m1128 545.4-12.3 56.1-26 118.7-.8 6.5-9 75.3-6 50-4 33.5c-24.3 12.6-42 21.2-42 21.2s-1.5-7.6-4-17.3a372.9 372.9 0 0 1-75.6 15.3c9.8 10.9-141.1-266.1-146.9-303.2a3614.4 3614.4 0 0 1-9-61.4l112.4-49.7c8 18.6 45.3 24.1 45.3 24.1 30.9-2 55.1-18.4 55.1-18.4Z"
-        />
-        <path
-          fill="#090814"
-          d="m1017.8 328.4 13.7-5.5s-28.7-31.7-68.7-29l11.2-12.3s-27.5-11-52.5 17.9c-13 15.2-28.3 33-37.8 53.1H869l6.1 13.6-21.5 13.5 22.1-2.4a75.6 75.6 0 0 0-.6 22.4 29 29 0 0 0 10.6 19s17.1-35.3 17.1-40.8v13.8s13.7-12.4 13.7-20.7l7.5 9.7 3.8-15.2 46.2 15.2-7.5-12.4 28.8 4.1-11.3-15.1s32.5 17.9 33.8 33 10.8 29.5 10.8 29.5 25.4-70.8-10.8-91.4Z"
-        />
-        <path
-          fill="currentColor"
-          d="M1208 803.3c-7.5 8.2-26 20.4-48.5 33.6a2319.9 2319.9 0 0 1-89.5 48.6c-24.4 12.6-42.1 21.2-42.1 21.2s-1.5-7.6-4-17.3c-3.3-13-8.2-30-13.3-37.4l-.6-.8c-1.5-2-3-3-4.5-3L1080 802l32.2-20-23.2-55.3-29-69.4 17.5-55.8 17.6-56h33s11 23.8 25 57.1l6.5 15.5c28.7 70 64.6 167.8 48.5 185.2ZM875.5 851.3a47.8 47.8 0 0 0-8.1.7c-23 4-29.8 24.7-31.7 38.7a71.8 71.8 0 0 0-.7 12.8L814.7 888l-7.4-5.7a134.9 134.9 0 0 1-47.5-30.2 238.5 238.5 0 0 1-32-38 334.1 334.1 0 0 1-24.6-42.3 31.2 31.2 0 0 1-.5-26.5l25.2-56 36.7-82q.4-2.9 1-5.7c7.2-41.8 26.8-60.3 26.8-60.3h14.9l10 60.3 12.4 75L821 705l-20.1 65.4 19.2 20.9Z"
-        />
-        <path
-          fill="#090814"
-          d="M1147.2 629.7H980.8v-3.4h-75.5v3.4H738.2a11.3 11.3 0 0 0-11.3 11.3v228a11.3 11.3 0 0 0 11.3 11.2h409a11.3 11.3 0 0 0 11.3-11.3V641a11.3 11.3 0 0 0-11.3-11.3Z"
-        />
-        <circle
-          cx="25"
-          cy="25"
-          r="25"
-          fill="#fff"
-          transform="translate(921.4 684.3)"
-        />
-      </g>
-    </g>
-  </svg>
-</template>

+ 18 - 5
src/layout/aside/component/Menu.vue → src/layout/aside/component/SidebarMenu.vue

@@ -1,11 +1,12 @@
-<script setup lang="ts">
+<script setup lang="tsx">
+import { isFunction } from 'lodash-es'
 import { NMenu, NScrollbar } from 'naive-ui'
-import { ref, useTemplateRef, watch } from 'vue'
+import { h, ref, useTemplateRef, watch } from 'vue'
 
 import router from '@/router'
 import { usePreferencesStore, useUserStore } from '@/stores'
 
-import type { MenuInst } from 'naive-ui'
+import type { MenuInst, MenuProps } from 'naive-ui'
 
 const preferencesStore = usePreferencesStore()
 
@@ -15,6 +16,14 @@ const menuRef = useTemplateRef<MenuInst>('menuRef')
 
 const menuActiveKey = ref('')
 
+const renderIcon: MenuProps['renderIcon'] = (option) => {
+  return isFunction(option.icon)
+    ? h(option.icon, {
+        class: 'size-5',
+      })
+    : null
+}
+
 watch(
   () => router.currentRoute.value,
   (newRoute) => {
@@ -30,11 +39,15 @@ watch(
   <NScrollbar>
     <NMenu
       ref="menuRef"
-      :collapsed-width="preferencesStore.preferences.menu.width"
-      :collapsed="preferencesStore.preferences.menu.collapsed"
+      :collapsed-width="preferencesStore.preferences.sidebarMenu.width"
+      :collapsed="preferencesStore.preferences.sidebarMenu.collapsed"
       :collapsed-icon-size="20"
       :value="menuActiveKey"
       :options="userStore.menuList"
+      :render-icon="renderIcon"
+      :dropdown-props="{
+        size: 'medium',
+      }"
     />
   </NScrollbar>
 </template>

+ 14 - 54
src/layout/aside/component/UserCard.vue → src/layout/aside/component/SidebarUserPanel.vue

@@ -1,83 +1,49 @@
 <script setup lang="ts">
-import { useMessage, NDropdown, NAvatar } from 'naive-ui'
-import { h } from 'vue'
+import { useMessage } from 'naive-ui'
 
 import { ButtonAnimation } from '@/components'
+import Avatar from '@/components/Avatar.vue'
+import UserDropdown from '@/components/UserDropdown.vue'
 import { usePreferencesStore, useUserStore } from '@/stores'
 
-import DefaultAvatar from './Avatar.vue'
-
 const preferencesStore = usePreferencesStore()
 const userStore = useUserStore()
 const message = useMessage()
 
-const userDropdownOptions = [
-  {
-    icon: () => h('span', { class: 'iconify ph--user size-5' }),
-    key: 'user',
-    label: '个人中心',
-  },
-  {
-    icon: () => h('span', { class: 'iconify ph--sign-out size-5' }),
-    key: 'signOut',
-    label: '退出登录',
-  },
-]
-
 const handleUserCardClick = () => {
   message.info('你可以把它设计成有背景的User Card')
 }
-
-const onUserDropdownSelected = (key: string) => {
-  switch (key) {
-    case 'user':
-      message.info('点击了个人中心')
-      break
-    case 'signOut':
-      userStore.cleanup()
-      break
-  }
-}
 </script>
 <template>
   <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.menu.collapsed
+      preferencesStore.preferences.sidebarMenu.collapsed
         ? 'mx-2 rounded'
         : 'mx-4 rounded-xl bg-neutral-150 py-3.5 pr-2.5 pl-3.5 dark:bg-neutral-800'
     "
     @click="handleUserCardClick"
   >
-    <NDropdown
-      :options="userDropdownOptions"
-      show-arrow
-      @select="onUserDropdownSelected"
+    <UserDropdown
       placement="right-end"
-      :disabled="!preferencesStore.preferences.menu.collapsed"
+      :disabled="!preferencesStore.preferences.sidebarMenu.collapsed"
     >
       <div
         class="grid place-items-center overflow-hidden rounded-full transition-[margin,padding]"
-        :class="preferencesStore.preferences.menu.collapsed ? 'mr-0 px-2 py-1.5' : 'mr-2'"
+        :class="preferencesStore.preferences.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.menu.collapsed ? 'size-8' : 'size-10'"
+          :class="preferencesStore.preferences.sidebarMenu.collapsed ? 'size-8' : 'size-10'"
         >
-          <NAvatar
-            round
-            src="src path"
+          <Avatar
             size="large"
             class="aspect-square"
             style="height: unset"
-          >
-            <template #fallback>
-              <DefaultAvatar class="text-primary" />
-            </template>
-          </NAvatar>
+          />
         </div>
       </div>
-    </NDropdown>
+    </UserDropdown>
     <Transition
       type="transition"
       enter-active-class="transition-[grid-template-columns] duration-150"
@@ -89,7 +55,7 @@ const onUserDropdownSelected = (key: string) => {
     >
       <div
         class="grid flex-1 overflow-hidden"
-        v-show="!preferencesStore.preferences.menu.collapsed"
+        v-show="!preferencesStore.preferences.sidebarMenu.collapsed"
       >
         <div class="flex min-w-0 items-center overflow-hidden">
           <div class="flex flex-1 flex-col gap-y-px">
@@ -101,20 +67,14 @@ const onUserDropdownSelected = (key: string) => {
             </span>
           </div>
 
-          <NDropdown
-            trigger="click"
-            :options="userDropdownOptions"
-            show-arrow
-            @select="onUserDropdownSelected"
-            placement="top"
-          >
+          <UserDropdown placement="top">
             <ButtonAnimation
               animation="rotate"
               title="设置"
             >
               <span class="iconify text-neutral-500 ph--gear dark:text-neutral-450" />
             </ButtonAnimation>
-          </NDropdown>
+          </UserDropdown>
         </div>
       </div>
     </Transition>

+ 12 - 12
src/layout/aside/index.vue

@@ -3,8 +3,8 @@ import { computed } from 'vue'
 
 import { usePreferencesStore } from '@/stores'
 
-import Menu from './component/Menu.vue'
-import UserCard from './component/UserCard.vue'
+import SidebarMenu from './component/SidebarMenu.vue'
+import SidebarUserPanel from './component/SidebarUserPanel.vue'
 
 defineOptions({
   name: 'AsideLayout',
@@ -13,28 +13,28 @@ defineOptions({
 const preferencesStore = usePreferencesStore()
 
 const menuCollapseWidth = computed(() => {
-  return preferencesStore.preferences.menu.collapsed
-    ? preferencesStore.preferences.menu.width
-    : preferencesStore.preferences.menu.maxWidth
+  return preferencesStore.preferences.sidebarMenu.collapsed
+    ? preferencesStore.preferences.sidebarMenu.width
+    : preferencesStore.preferences.sidebarMenu.maxWidth
 })
 
 function handleCollapseClick() {
   preferencesStore.modify({
-    menu: {
-      collapsed: !preferencesStore.preferences.menu.collapsed,
+    sidebarMenu: {
+      collapsed: !preferencesStore.preferences.sidebarMenu.collapsed,
     },
   })
 }
 </script>
 <template>
-  <div
+  <aside
     class="relative flex h-full flex-col justify-between gap-y-4 border-r border-naive-border bg-naive-card pb-4 transition-[background-color,border-color,width]"
     :style="{
       width: `${menuCollapseWidth}px`,
     }"
   >
-    <Menu />
-    <UserCard />
+    <SidebarMenu />
+    <SidebarUserPanel />
     <div
       class="absolute top-1/2 right-0 z-50 grid size-6 translate-x-1/2 -translate-y-1/2 cursor-pointer place-items-center rounded-full border border-naive-border bg-white transition-[background-color,border-color] hover:bg-neutral-50 dark:bg-neutral-750 dark:hover:bg-neutral-700"
       @click="handleCollapseClick"
@@ -42,9 +42,9 @@ function handleCollapseClick() {
       <span
         class="iconify size-4.5 transition-[color,rotate] ph--caret-left dark:text-neutral-400"
         :class="{
-          'rotate-180': preferencesStore.preferences.menu.collapsed,
+          'rotate-180': preferencesStore.preferences.sidebarMenu.collapsed,
         }"
       />
     </div>
-  </div>
+  </aside>
 </template>

+ 14 - 0
src/layout/header/AvatarDropdown.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+import { ButtonAnimation } from '@/components'
+import Avatar from '@/components/Avatar.vue'
+import UserDropdown from '@/components/UserDropdown.vue'
+</script>
+<template>
+  <div class="ml-4">
+    <UserDropdown placement="bottom-end">
+      <ButtonAnimation animation="beat">
+        <Avatar src="src path" />
+      </ButtonAnimation>
+    </UserDropdown>
+  </div>
+</template>

+ 9 - 9
src/layout/header/Breadcrumb.vue

@@ -47,38 +47,38 @@ function isCurrentRoute(name: RouteRecordNameGeneric) {
     leave-from-class="grid-cols-[1fr]"
   >
     <li
-      v-for="item in routerBreadcrumb"
-      :key="item.path"
+      v-for="{ children, meta, name, path } in routerBreadcrumb"
+      :key="path"
       class="grid shrink-0 justify-start overflow-hidden"
     >
       <div
         class="flex min-w-0 items-center"
         :class="{
-          'not-hover:text-[var(--text-color-3)]': !isCurrentRoute(item.name),
+          'not-hover:text-[var(--text-color-3)]': !isCurrentRoute(name),
         }"
       >
         <NDropdown
-          :options="resolveDropdownOptions(item.children)"
+          :options="resolveDropdownOptions(children)"
           placement="bottom-start"
           @select="onDropdownSelected"
         >
           <div
             class="flex shrink-0 items-center gap-x-1.5 rounded px-1.5 py-1 transition-[background-color,color]"
             :class="{
-              'cursor-pointer hover:bg-[var(--button-color-2-hover)]': !isCurrentRoute(item.name),
+              'cursor-pointer hover:bg-[var(--button-color-2-hover)]': !isCurrentRoute(name),
             }"
           >
             <span
-              v-if="item.meta?.icon"
+              v-if="meta?.icon"
               class="size-5"
-              :class="item.meta?.icon"
+              :class="meta?.icon"
             />
-            {{ item.meta?.title }}
+            {{ meta?.title }}
           </div>
         </NDropdown>
         <span
           class="iconify-[fluent--slash-forward-20-regular] w-3.5 text-[var(--text-color-3)]"
-          v-if="!isCurrentRoute(item.name)"
+          v-if="!isCurrentRoute(name)"
         />
       </div>
     </li>

+ 47 - 16
src/layout/header/LogoArea.vue

@@ -1,32 +1,61 @@
 <script setup lang="ts">
-import { computed } from 'vue'
+import { nextTick, ref, watch } from 'vue'
 
 import Logo from '@/components/Logo.vue'
-import { usePreferencesStore } from '@/stores'
+import { usePreferencesStore, DEFAULT_PREFERENCES_OPTIONS } from '@/stores'
 
 const APP_NAME = import.meta.env.VITE_APP_NAME
 
 const preferencesStore = usePreferencesStore()
 
-const collapseWidth = computed(() => {
-  return preferencesStore.preferences.menu.collapsed
-    ? preferencesStore.preferences.menu.width
-    : preferencesStore.preferences.menu.maxWidth
-})
+const logoAreaWrapRef = ref<HTMLElement | null>(null)
+
+const collapseWidth = ref(0)
+
+watch(
+  () => [
+    preferencesStore.preferences.navigationMode,
+    preferencesStore.preferences.sidebarMenu.collapsed,
+  ],
+  ([navigationMode, isCollapsed]) => {
+    if (navigationMode === 'horizontal') {
+      nextTick(() => {
+        collapseWidth.value = logoAreaWrapRef.value?.clientWidth ?? 0
+      })
+    } else {
+      const { width, maxWidth } = preferencesStore.preferences.sidebarMenu
+      const { width: defaultWidth, maxWidth: defaultMaxWidth } =
+        DEFAULT_PREFERENCES_OPTIONS.sidebarMenu
+      collapseWidth.value = isCollapsed ? width || defaultWidth : maxWidth || defaultMaxWidth
+    }
+  },
+  {
+    immediate: true,
+  },
+)
 </script>
 <template>
   <div
-    class="shrink-0 border-r border-naive-border transition-[border-color,width] max-sm:border-0"
-    :style="{
-      width: `${collapseWidth}px`,
-    }"
+    class="shrink-0 border-r transition-[border-color,width] max-sm:border-0"
+    :style="
+      collapseWidth > 0 && {
+        width: `${collapseWidth}px`,
+      }
+    "
+    :class="
+      preferencesStore.preferences.navigationMode === 'sidebar'
+        ? 'border-naive-border'
+        : 'border-transparent'
+    "
   >
     <div
-      class="flex h-full items-center justify-center transition-[opacity,padding]"
+      ref="logoAreaWrapRef"
+      class="flex h-full items-center transition-[opacity,padding]"
       :class="[
-        preferencesStore.preferences.menu.collapsed ? 'px-0' : 'px-4',
+        preferencesStore.preferences.sidebarMenu.collapsed ? 'px-3' : 'px-4',
         {
           'opacity-0': !preferencesStore.preferences.showLogo,
+          'w-fit': preferencesStore.preferences.navigationMode === 'horizontal',
         },
       ]"
     >
@@ -34,10 +63,12 @@ const collapseWidth = computed(() => {
         <Logo />
       </div>
       <div
-        class="flex-1 overflow-hidden transition-[margin-left,max-width]"
-        :class="preferencesStore.preferences.menu.collapsed ? 'ml-0 max-w-0' : 'ml-4 max-w-44'"
+        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'
+        "
       >
-        <h1 class="truncate text-xl">
+        <h1 class="shrink-0 text-xl">
           {{ APP_NAME }}
         </h1>
       </div>

+ 90 - 0
src/layout/header/actions/component/LayoutThumbnail.vue

@@ -0,0 +1,90 @@
+<script setup lang="ts">
+import { usePersonalization } from '@/composables'
+import { usePreferencesStore } from '@/stores'
+
+const { color } = usePersonalization()
+const preferencesStore = usePreferencesStore()
+</script>
+<template>
+  <div
+    class="flex justify-center gap-6"
+    :style="{
+      '--primary-color': color,
+    }"
+  >
+    <div
+      class="flex h-16 w-20 cursor-pointer flex-col rounded border border-neutral-350 outline-offset-4 transition-[outline] dark:border-neutral-650"
+      :class="
+        preferencesStore.preferences.navigationMode === 'sidebar'
+          ? 'outline-2 outline-primary/50'
+          : 'outline-2 outline-transparent hover:outline-primary/30'
+      "
+      @click="preferencesStore.modify({ navigationMode: 'sidebar' })"
+    >
+      <div class="flex h-2.5 border-b border-neutral-350 dark:border-neutral-650">
+        <div class="h-full w-5 shrink-0 border-r border-neutral-350 dark:border-neutral-650"></div>
+        <div class="flex w-full items-center justify-between p-0.5">
+          <div class="flex items-center gap-x-[1px]">
+            <div
+              v-for="i in 2"
+              :key="i"
+              class="size-1 rounded-full border border-primary"
+            />
+            <div class="h-1 w-2 border border-primary" />
+          </div>
+          <div class="flex items-center gap-x-[1px]">
+            <div
+              v-for="i in 4"
+              :key="i"
+              class="size-1 rounded-full border border-neutral-350 dark:border-neutral-650"
+            />
+          </div>
+        </div>
+      </div>
+      <div class="flex flex-1">
+        <div class="flex w-5 flex-col border-r border-neutral-350 p-0.5 dark:border-neutral-650">
+          <div class="mb-0.5 h-2 w-full flex-1 rounded-xs border border-primary" />
+          <div class="h-2 w-full rounded-xs border border-primary" />
+        </div>
+        <div class="flex flex-1">
+          <div class="h-1 w-full border-b border-neutral-350 dark:border-neutral-650"></div>
+        </div>
+      </div>
+    </div>
+    <div
+      class="flex h-16 w-20 cursor-pointer flex-col rounded border border-neutral-350 outline-offset-4 transition-[outline] duration-300 dark:border-neutral-650"
+      :class="
+        preferencesStore.preferences.navigationMode === 'horizontal'
+          ? 'outline-2 outline-primary/50'
+          : 'outline-2 outline-transparent hover:outline-primary/30'
+      "
+      @click="preferencesStore.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>
+        <div class="flex w-full items-center justify-between p-0.5">
+          <div class="flex items-center gap-x-[1px]">
+            <div
+              v-for="i in 6"
+              :key="i"
+              class="size-1 rounded-full border border-primary"
+            />
+          </div>
+          <div class="flex items-center gap-x-[1px]">
+            <div
+              v-for="i in 3"
+              :key="i"
+              class="size-1 rounded-full border border-neutral-350 dark:border-neutral-650"
+            />
+            <div class="size-1 rounded-full border border-primary" />
+          </div>
+        </div>
+      </div>
+      <div class="flex flex-1">
+        <div class="flex flex-1">
+          <div class="h-1 w-full border-b border-neutral-350 dark:border-neutral-650"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>

+ 2 - 2
src/layout/header/actions/component/NoiseModal.vue

@@ -16,7 +16,7 @@ const sliderRange = reactive({
   max: 0.1,
 })
 
-const onSliderUpdata: SliderProps['onUpdateValue'] = (opacity) => {
+const onSliderUpdate: SliderProps['onUpdateValue'] = (opacity) => {
   preferencesStore.modify({
     noiseOpacity: opacity,
   })
@@ -31,7 +31,7 @@ watchEffect(() => {
     <NSlider
       v-model:value="opacity"
       v-bind="sliderRange"
-      @update-value="onSliderUpdata"
+      @update-value="onSliderUpdate"
     />
     <NInputNumber
       v-model:value="opacity"

+ 9 - 2
src/layout/header/actions/component/ThemeDropdown.vue → src/layout/header/actions/component/ThemePopselect.vue

@@ -8,7 +8,13 @@ import { usePersonalization } from '@/composables'
 import type { Theme } from '@/composables'
 import type { PopoverProps } from 'naive-ui'
 
-const props = defineProps</* @vue-ignore */ PopoverProps>()
+interface ThemePopselectProps extends /* @vue-ignore */ PopoverProps {}
+
+defineProps<ThemePopselectProps>()
+
+defineOptions({
+  inheritAttrs: false,
+})
 
 const { setTheme, theme } = usePersonalization()
 
@@ -60,8 +66,9 @@ function renderSelectLabel(option: (typeof themeDropdownOptions)[number]) {
 </script>
 <template>
   <NPopselect
-    v-bind="props"
+    class="p-0.5"
     trigger="click"
+    v-bind="$attrs"
     v-model:value="theme"
     :options="themeDropdownOptions"
     :render-label="renderSelectLabel"

+ 2 - 2
src/layout/header/actions/index.vue

@@ -4,7 +4,7 @@ import { ButtonAnimation } from '@/components'
 import FullScreen from './component/FullScreen.vue'
 import PreferencesDrawer from './component/PreferencesDrawer.vue'
 import SignOut from './component/SignOut.vue'
-import ThemeDropdown from './component/ThemeDropdown.vue'
+import ThemePopselect from './component/ThemePopselect.vue'
 
 defineOptions({
   name: 'Actions',
@@ -21,7 +21,7 @@ defineOptions({
       <span class="iconify-[mdi--github]" />
     </ButtonAnimation>
     <FullScreen />
-    <ThemeDropdown />
+    <ThemePopselect />
     <PreferencesDrawer />
     <SignOut />
   </div>

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

@@ -1,9 +1,11 @@
 <script setup lang="ts">
-import CollapseTransition from '@/components/collapse-transition/CollapseTransition.vue'
+import { CollapseTransition } from '@/components'
 import { usePreferencesStore } from '@/stores'
 
 import Actions from './actions/index.vue'
+import AvatarDropdown from './AvatarDropdown.vue'
 import Breadcrumb from './Breadcrumb.vue'
+import HorizontalMenu from './HorizontalMenu.vue'
 import LogoArea from './LogoArea.vue'
 import Navigation from './Navigation.vue'
 
@@ -18,19 +20,31 @@ const preferencesStore = usePreferencesStore()
     class="flex border-b border-naive-border bg-naive-card transition-[background-color,border-color]"
   >
     <LogoArea />
-    <div class="flex flex-1 items-center p-4">
-      <div class="flex flex-1 items-center">
-        <CollapseTransition :display="preferencesStore.preferences.showNavigation">
+    <div class="flex flex-1 items-center px-4 py-3.5">
+      <div class="flex h-9 flex-1 items-center">
+        <CollapseTransition
+          :display="
+            preferencesStore.preferences.showNavigation &&
+            preferencesStore.preferences.navigationMode === 'sidebar'
+          "
+        >
           <Navigation />
         </CollapseTransition>
         <CollapseTransition
-          :display="preferencesStore.preferences.showBreadcrumb"
+          :display="
+            preferencesStore.preferences.showBreadcrumb &&
+            preferencesStore.preferences.navigationMode === 'sidebar'
+          "
           contentTag="nav"
         >
           <Breadcrumb />
         </CollapseTransition>
+        <CollapseTransition :display="preferencesStore.preferences.navigationMode === 'horizontal'">
+          <HorizontalMenu />
+        </CollapseTransition>
       </div>
-      <Actions class="gap-x-3" />
+      <Actions class="gap-x-3 pl-4" />
+      <AvatarDropdown v-if="preferencesStore.preferences.navigationMode === 'horizontal'" />
     </div>
   </header>
 </template>

+ 10 - 5
src/layout/index.vue

@@ -29,7 +29,7 @@ const { layoutSlideDirection, setLayoutSlideDirection } = useInjection(layoutInj
 
 const layoutTranslateOffset = computed(() => {
   return layoutSlideDirection.value === 'right'
-    ? preferencesStore.preferences.menu.maxWidth || 0
+    ? preferencesStore.preferences.sidebarMenu.maxWidth || 0
     : layoutSlideDirection.value === 'left'
       ? -64
       : 0
@@ -40,7 +40,7 @@ watch(
   (isSmallScreen) => {
     if (isSmallScreen) {
       preferencesStore.modify({
-        menu: {
+        sidebarMenu: {
           collapsed: false,
         },
       })
@@ -73,14 +73,20 @@ watch(
       <HeaderLayout v-if="!isSmallScreen" />
       <MobileHeader v-else />
       <div class="flex flex-1 overflow-hidden">
-        <AsideLayout v-if="!isSmallScreen" />
+        <CollapseTransition
+          v-if="!isSmallScreen"
+          :display="preferencesStore.preferences.navigationMode === 'sidebar'"
+          content-class="min-h-0"
+        >
+          <AsideLayout />
+        </CollapseTransition>
+
         <div class="flex flex-1 flex-col overflow-hidden">
           <CollapseTransition
             v-if="!isSmallScreen"
             :display="!isEmpty(tabsStore.tabs) && preferencesStore.preferences.showTabs"
             direction="horizontal"
             :render-content="false"
-            container-class="shrink-0 items-baseline"
           >
             <Tabs />
           </CollapseTransition>
@@ -103,7 +109,6 @@ watch(
             :display="preferencesStore.preferences.showFooter"
             direction="horizontal"
             :render-content="false"
-            container-class="shrink-0 items-baseline"
           >
             <FooterLayout />
           </CollapseTransition>

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

@@ -16,20 +16,21 @@ defineOptions({
 })
 
 const { isSmallScreen } = useInjection(mediaQueryInjectionKey)
+
 const { shouldRefreshRoute, layoutSlideDirection, setLayoutSlideDirection } =
   useInjection(layoutInjectionKey)
 
-const layoutRouteRedirect = router.getRoutes().find((item) => item.name === 'layout')?.redirect
-
 const tabsStore = useTabsStore()
 
 const preferencesStore = usePreferencesStore()
 
 const { createTab, setTabActivePath } = tabsStore
 
+const isMounted = ref(false)
+
 const navigationTransitionName = ref('scale')
 
-const isMounted = ref(false)
+const layoutRouteRedirect = router.getRoutes().find((item) => item.name === 'layout')?.redirect
 
 const keepAliveTabs = computed(() => {
   return tabsStore.tabs.filter((tab) => tab.keepAlive).map((tab) => tab.componentName ?? '')

+ 4 - 4
src/layout/mobile/MobileLeftAside.vue

@@ -2,8 +2,8 @@
 import { useInjection } from '@/composables'
 import { layoutInjectionKey } from '@/injection'
 
-import Menu from '../aside/component/Menu.vue'
-import UserCard from '../aside/component/UserCard.vue'
+import SidebarMenu from '../aside/component/SidebarMenu.vue'
+import SidebarUserPanel from '../aside/component/SidebarUserPanel.vue'
 import LogoArea from '../header/LogoArea.vue'
 
 const { layoutSlideDirection } = useInjection(layoutInjectionKey)
@@ -16,7 +16,7 @@ const { layoutSlideDirection } = useInjection(layoutInjectionKey)
     }"
   >
     <LogoArea />
-    <Menu />
-    <UserCard />
+    <SidebarMenu />
+    <SidebarUserPanel />
   </div>
 </template>

+ 32 - 27
src/router/helper.ts

@@ -1,8 +1,13 @@
-import { isArray, isEmpty, isString, pickBy, omit } from 'lodash-es'
+import { isEmpty, isString, pickBy, omit } from 'lodash-es'
 import { h } from 'vue'
 import { RouterLink } from 'vue-router'
 
-import type { MenuProps, MenuDividerOption, MenuOption, MenuGroupOption } from 'naive-ui'
+import type {
+  MenuProps,
+  MenuDividerOption as MenuDividerOptionRaw,
+  MenuOption as MenuOptionRaw,
+  MenuGroupOption as MenuGroupOptionRaw,
+} from 'naive-ui'
 import type { RouteRecordRaw } from 'vue-router'
 
 type NoIndex<T> = {
@@ -26,43 +31,43 @@ type RouteOption = Omit<CustomRouteRecordRaw, 'children'> & {
   type?: never
 }
 
-type MergeMenuOption = ReplaceKeys<
-  MenuOption,
+type MenuOption = ReplaceKeys<
+  MenuOptionRaw,
   {
     icon: string
-    children?: MergeMenuMixedOptions[]
+    children?: MenuMixedOptions[]
   }
 > &
   RouteOption
 
-type MergeMenuGroup = NoIndex<
+type MenuGroup = NoIndex<
   ReplaceKeys<
-    MenuGroupOption,
+    MenuGroupOptionRaw,
     {
-      children?: Array<MergeMenuOption | MergeMenuDivider>
+      children?: Array<MenuOption | MenuDivider>
     }
   >
 >
 
-type MergeMenuDivider = NoIndex<MenuDividerOption>
+type MenuDivider = NoIndex<MenuDividerOptionRaw>
 
-export type MergeMenuMixedOptions = MergeMenuOption | MergeMenuGroup | MergeMenuDivider
+export type MenuMixedOptions = MenuOption | MenuGroup | MenuDivider
 
-export function resolveMenu(options: MergeMenuMixedOptions[], parentDisabled = false) {
-  const menuOption: MenuProps['options'] = []
+export function resolveMenu(options: MenuMixedOptions[], parentDisabled = false) {
+  const menuOptions: MenuProps['options'] = []
 
   options.forEach((item) => {
     if (!item.type || item.type === 'group') {
       const { children, name, path, label, icon, key, disabled, extra, props, show, type } =
-        item as MergeMenuOption & { label: string }
+        item as MenuOption & { label: string }
 
       const mergedDisabled = parentDisabled || disabled
 
-      const renderIcon = icon ? () => h('span', { class: `${icon} size-6` }) : null
+      const renderIcon = icon ? () => h('span', { class: `${icon}` }) : null
 
       const menu = pickBy(
         {
-          key: key || (name as string) || (path as string),
+          key: key || name || path,
           icon: renderIcon,
           label,
           disabled,
@@ -70,11 +75,12 @@ export function resolveMenu(options: MergeMenuMixedOptions[], parentDisabled = f
           props,
           show,
           type,
+          name,
         },
         (v) => v !== undefined,
       ) as NonNullable<MenuProps['options']>[number]
 
-      if (isArray(children) && !isEmpty(children)) {
+      if (Array.isArray(children) && !isEmpty(children)) {
         menu.children = resolveMenu(children, mergedDisabled)
       } else {
         menu.label = mergedDisabled
@@ -82,27 +88,27 @@ export function resolveMenu(options: MergeMenuMixedOptions[], parentDisabled = f
           : () => h(RouterLink, { to: { name } }, { default: () => label })
       }
 
-      menuOption.push(menu)
+      menuOptions.push(menu)
     } else {
-      menuOption.push(item)
+      menuOptions.push(item)
     }
   })
 
-  return menuOption
+  return menuOptions
 }
 
-export function resolveRoute(options: MergeMenuMixedOptions[]) {
+export function resolveRoute(options: MenuMixedOptions[]) {
   const modules = import.meta.glob('@/views/**/*.vue')
 
   const routeOptions: RouteRecordRaw[] = []
 
-  function flattenOptions(options: MergeMenuMixedOptions[]): MergeMenuMixedOptions[] {
+  function flattenOptions(options: MenuMixedOptions[]): MenuMixedOptions[] {
     return options.flatMap((item) => {
       if (item.type === 'divider') {
         return []
       }
 
-      if (item.type === 'group' && isArray(item.children) && !isEmpty(item.children)) {
+      if (item.type === 'group' && Array.isArray(item.children) && !isEmpty(item.children)) {
         return flattenOptions(item.children)
       }
 
@@ -111,10 +117,9 @@ export function resolveRoute(options: MergeMenuMixedOptions[]) {
   }
 
   flattenOptions(options).forEach((item) => {
-    const { label, icon, meta, component, children, disabled, ...rest } =
-      item as MergeMenuOption & {
-        label: string
-      }
+    const { label, icon, meta, component, children, disabled, ...rest } = item as MenuOption & {
+      label: string
+    }
 
     if (!disabled) {
       let compModule: (() => Promise<unknown>) | null = null
@@ -140,7 +145,7 @@ export function resolveRoute(options: MergeMenuMixedOptions[]) {
         ['type', 'label', 'icon', 'disabled', 'extra', 'props', 'show', 'key'],
       ) as RouteRecordRaw
 
-      if (isArray(children) && !isEmpty(children)) {
+      if (Array.isArray(children) && !isEmpty(children)) {
         route.children = resolveRoute(children)
       }
 

+ 3 - 2
src/router/record.ts

@@ -1,6 +1,6 @@
-import type { MergeMenuMixedOptions } from './helper'
+import type { MenuMixedOptions } from './helper'
 
-export const routeRecordRaw: MergeMenuMixedOptions[] = [
+export const routeRecordRaw: MenuMixedOptions[] = [
   {
     path: 'dashboard',
     name: 'dashboard',
@@ -185,6 +185,7 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
     name: 'feedback',
     icon: 'iconify-[ph--messenger-logo]',
     label: '反馈组件',
+    disabled: true,
     meta: {
       componentName: 'Feedback',
       showTab: true,

+ 2 - 2
src/stores/user.ts

@@ -6,7 +6,7 @@ import router from '@/router'
 import { resolveMenu, resolveRoute } from '@/router/helper'
 import { routeRecordRaw } from '@/router/record'
 
-import type { MergeMenuMixedOptions } from '@/router/helper'
+import type { MenuMixedOptions } from '@/router/helper'
 import type { MenuProps } from 'naive-ui'
 import type { RouteRecordRaw } from 'vue-router'
 
@@ -38,7 +38,7 @@ export const useUserStore = defineStore('userStore', () => {
   }
 
   async function resolveMenuList() {
-    const res = await new Promise<MergeMenuMixedOptions[]>((resolve) => {
+    const res = await new Promise<MenuMixedOptions[]>((resolve) => {
       if (token.value?.includes('admin')) {
         resolve(routeRecordRaw)
       } else {

+ 10 - 1
src/theme/common.ts

@@ -34,11 +34,20 @@ export function commonThemeOverrides(primaryColor = ''): GlobalThemeOverrides {
     Drawer: {
       borderRadius: 0,
     },
+    Dropdown: {
+      padding: '6px 2px',
+    },
     Message: {
       iconMargin: '0 6px 0 0',
       padding: '10px 18px',
     },
-
+    Menu: {
+      peers: {
+        Dropdown: {
+          padding: '6px 2px',
+        },
+      },
+    },
     Popconfirm: {
       iconSize: '20px',
     },

+ 20 - 12
src/views/dashboard/index.vue

@@ -1051,7 +1051,7 @@ function resizeAllCharts() {
 }
 
 watch(
-  () => preferencesStore.preferences.menu.collapsed,
+  () => preferencesStore.preferences.sidebarMenu.collapsed,
   () => {
     if (collapseResizeTimeout !== null) {
       clearTimeout(collapseResizeTimeout)
@@ -1122,44 +1122,52 @@ watch([isDark, color], () => {
   <ContentWrapper content-class="flex flex-col gap-y-4 max-sm:gap-y-2">
     <div class="grid grid-cols-1 gap-4 max-sm:gap-2 md:grid-cols-2 lg:grid-cols-4">
       <div
-        v-for="item in cardList"
-        :key="item.title"
+        v-for="{
+          title,
+          value,
+          precision,
+          percentage,
+          description,
+          iconBgClass,
+          iconClass,
+        } in cardList"
+        :key="title"
         class="flex items-center justify-between gap-x-4 overflow-hidden rounded bg-naive-card p-6 shadow-xs transition-[background-color]"
       >
         <div class="flex-1">
-          <span class="text-sm font-medium text-neutral-450">{{ item.title }}</span>
+          <span class="text-sm font-medium text-neutral-450">{{ title }}</span>
           <div class="mt-1 mb-1.5 flex gap-x-4 text-2xl text-neutral-700 dark:text-neutral-400">
             <NNumberAnimation
-              :to="item.value"
+              :to="value"
               show-separator
-              :precision="item.precision"
+              :precision="precision"
             />
           </div>
           <div class="flex items-center">
             <div
               class="flex items-center gap-x-0.5 rounded-xs px-1.5 py-0.5 text-xs transition-[background-color,color]"
               :class="
-                item.percentage > 0
+                percentage > 0
                   ? 'bg-green-100 text-green-600 dark:bg-green-500/20 dark:text-green-400'
                   : 'bg-rose-100 text-rose-600 dark:bg-rose-500/20 dark:text-rose-400'
               "
             >
               <span
-                :class="item.percentage > 0 ? 'iconify ph--arrow-up' : 'iconify ph--arrow-down'"
+                :class="percentage > 0 ? 'iconify ph--arrow-up' : 'iconify ph--arrow-down'"
               ></span>
-              <span class="font-medium">{{ Math.abs(item.percentage) }}%</span>
+              <span class="font-medium">{{ Math.abs(percentage) }}%</span>
             </div>
-            <span class="ml-2 text-neutral-500 dark:text-neutral-400">{{ item.description }}</span>
+            <span class="ml-2 text-neutral-500 dark:text-neutral-400">{{ description }}</span>
           </div>
         </div>
         <div>
           <div
             class="grid place-items-center rounded-full p-3"
-            :class="item.iconBgClass"
+            :class="iconBgClass"
           >
             <span
               class="size-7"
-              :class="item.iconClass"
+              :class="iconClass"
             />
           </div>
         </div>

+ 9 - 9
src/views/drag-drop/index.vue

@@ -147,11 +147,11 @@ watch(
               class="flex flex-col gap-2 rounded bg-neutral-500/5 p-4 select-none"
             >
               <div
-                v-for="item in baseList"
-                :key="item.id"
+                v-for="{ id, name } in baseList"
+                :key="id"
                 class="flex h-14 cursor-move items-center justify-center rounded bg-neutral-500/8 p-3"
               >
-                {{ item.name }}
+                {{ name }}
               </div>
             </VueDraggable>
           </NScrollbar>
@@ -197,11 +197,11 @@ watch(
               class="m-auto grid grid-cols-8 gap-2 rounded bg-neutral-500/5 p-4 select-none max-lg:grid-cols-4"
             >
               <div
-                v-for="item in gridList"
-                :key="item.id"
+                v-for="{ id, name } in gridList"
+                :key="id"
                 class="flex h-14 cursor-move items-center justify-center rounded bg-neutral-500/8 p-3"
               >
-                {{ item.name }}
+                {{ name }}
               </div>
             </VueDraggable>
           </NScrollbar>
@@ -245,11 +245,11 @@ watch(
                 :clone="cloneTask"
               >
                 <div
-                  v-for="item in taskList"
-                  :key="item.id"
+                  v-for="{ id, name } in taskList"
+                  :key="id"
                   class="flex h-14 cursor-move items-center justify-center rounded bg-neutral-500/8 p-3"
                 >
-                  {{ item.name }}
+                  {{ name }}
                 </div>
               </VueDraggable>
             </NScrollbar>

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

@@ -13,7 +13,7 @@ import {
 import topographySvg from '@/assets/topography.svg'
 import { usePersonalization, useInjection } from '@/composables'
 import { mediaQueryInjectionKey } from '@/injection'
-import ThemeDropdown from '@/layout/header/actions/component/ThemeDropdown.vue'
+import ThemePopselect from '@/layout/header/actions/component/ThemePopselect.vue'
 import router from '@/router'
 import { useUserStore } from '@/stores'
 import twc from '@/utils/tailwindColor'
@@ -167,7 +167,7 @@ onUnmounted(() => {
       >
         <div class="absolute top-0 left-0 z-50 flex w-full items-center justify-end gap-x-4 p-4">
           <ThemeColorPopover />
-          <ThemeDropdown />
+          <ThemePopselect />
         </div>
         <div>
           <div>