فهرست منبع

feat(Layout): Add topLeft layout

陈凯龙 3 سال پیش
والد
کامیت
71b1c5e10c

+ 16 - 1
src/components/Breadcrumb/src/Breadcrumb.vue

@@ -83,12 +83,27 @@ export default defineComponent({
 </script>
 
 <style lang="less" scoped>
-:deep(.el-breadcrumb__item) {
+:deep(.el-breadcrumb__item):not(:last-child) {
   display: flex;
 
   .el-breadcrumb__inner {
     display: flex;
     align-items: center;
+    color: var(--top-header-text-color);
+
+    &:hover {
+      color: var(--el-color-primary);
+    }
+  }
+}
+
+:deep(.el-breadcrumb__item):last-child {
+  .el-breadcrumb__inner {
+    color: rgba(255, 255, 255, 0.6);
+
+    &:hover {
+      color: rgba(255, 255, 255, 0.6);
+    }
   }
 }
 </style>

+ 6 - 0
src/components/Collapse/src/Collapse.vue

@@ -1,6 +1,11 @@
 <script setup lang="ts">
 import { computed, unref } from 'vue'
 import { useAppStore } from '@/store/modules/app'
+import { propTypes } from '@/utils/propTypes'
+
+defineProps({
+  color: propTypes.string.def('')
+})
 
 const appStore = useAppStore()
 
@@ -17,6 +22,7 @@ const toggleCollapse = () => {
     <Icon
       :size="18"
       :icon="collapse ? 'ant-design:menu-unfold-outlined' : 'ant-design:menu-fold-outlined'"
+      :color="color"
       class="cursor-pointer"
       @click="toggleCollapse"
     />

+ 0 - 3
src/components/ColorRadioPicker/index.ts

@@ -1,3 +0,0 @@
-import ColorRadioPicker from './src/ColorRadioPicker.vue'
-
-export { ColorRadioPicker }

+ 12 - 1
src/components/LocaleDropdown/src/LocaleDropdown.vue

@@ -3,6 +3,11 @@ import { computed, unref } from 'vue'
 import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
 import { useLocaleStore } from '@/store/modules/locale'
 import { useLocale } from '@/hooks/web/useLocale'
+import { propTypes } from '@/utils/propTypes'
+
+defineProps({
+  color: propTypes.string.def('')
+})
 
 const localeStore = useLocaleStore()
 
@@ -24,7 +29,13 @@ const setLang = (lang: LocaleType) => {
 
 <template>
   <ElDropdown trigger="click" @command="setLang">
-    <Icon :size="18" icon="ion:language-sharp" class="cursor-pointer" :class="$attrs.class" />
+    <Icon
+      :size="18"
+      icon="ion:language-sharp"
+      class="cursor-pointer"
+      :class="$attrs.class"
+      :color="color"
+    />
     <template #dropdown>
       <ElDropdownMenu>
         <ElDropdownItem v-for="item in langMap" :key="item.lang" :command="item.lang">

+ 13 - 4
src/components/Logo/src/Logo.vue

@@ -41,7 +41,7 @@ watch(
       {
         'v-logo__Top': layout !== 'classic'
       },
-      'flex h-[var(--logo-height)] items-center cursor-pointer pl-8px relative'
+      'flex !h-[var(--logo-height)] items-center cursor-pointer pl-8px relative'
     ]"
     to="/"
   >
@@ -49,9 +49,18 @@ watch(
       src="@/assets/imgs/logo.png"
       class="w-[calc(var(--logo-height)-10px)] h-[calc(var(--logo-height)-10px)]"
     />
-    <div v-if="show" class="text-[var(--logo-title-text-color)] ml-10px text-16px font-700">{{
-      title
-    }}</div>
+    <div
+      v-if="show"
+      :class="[
+        'ml-10px text-16px font-700',
+        {
+          'text-[var(--logo-title-text-color)]': layout === 'classic',
+          'text-[var(--top-header-text-color)]': layout === 'topLeft'
+        }
+      ]"
+    >
+      {{ title }}
+    </div>
   </router-link>
 </template>
 

+ 3 - 7
src/components/Menu/src/Menu.vue

@@ -7,23 +7,19 @@ import type { LayoutType } from '@/config/app'
 import { useRenderMenuItem } from './components/useRenderMenuItem'
 import { useRouter } from 'vue-router'
 import { isUrl } from '@/utils/is'
-import { Logo } from '@/components/Logo'
 
 export default defineComponent({
   name: 'Menu',
   setup() {
     const appStore = useAppStore()
 
-    // logo
-    const logo = computed(() => appStore.logo)
-
     const { push, currentRoute } = useRouter()
 
     const permissionStore = usePermissionStore()
 
     const menuMode = computed((): 'vertical' | 'horizontal' => {
       // 竖
-      const vertical: LayoutType[] = ['classic']
+      const vertical: LayoutType[] = ['classic', 'topLeft']
 
       if (vertical.includes(appStore.getLayout)) {
         return 'vertical'
@@ -64,8 +60,7 @@ export default defineComponent({
           'bg-[var(--left-menu-bg-color)]'
         ]}
       >
-        {logo.value ? <Logo></Logo> : undefined}
-        <ElScrollbar class={[{ '!h-[calc(100%-var(--logo-height))]': logo.value }]}>
+        <ElScrollbar>
           <ElMenu
             defaultActive={unref(activeMenu)}
             mode={unref(menuMode)}
@@ -103,6 +98,7 @@ export default defineComponent({
 }
 
 .@{prefix-cls} {
+  position: relative;
   transition: width var(--transition-time-02);
 
   &:after {

+ 10 - 1
src/components/Screenfull/src/Screenfull.vue

@@ -1,6 +1,11 @@
 <script setup lang="ts">
 import { Icon } from '@/components/Icon'
 import { useFullscreen } from '@vueuse/core'
+import { propTypes } from '@/utils/propTypes'
+
+defineProps({
+  color: propTypes.string.def('')
+})
 
 const { toggle, isFullscreen } = useFullscreen()
 
@@ -11,6 +16,10 @@ const toggleFullscreen = () => {
 
 <template>
   <div @click="toggleFullscreen">
-    <Icon :size="18" :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'" />
+    <Icon
+      :size="18"
+      :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
+      :color="color"
+    />
   </div>
 </template>

+ 37 - 1
src/components/Setting/src/Setting.vue

@@ -3,12 +3,13 @@ import { ElDrawer, ElDivider } from 'element-plus'
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { ThemeSwitch } from '@/components/ThemeSwitch'
-import { ColorRadioPicker } from '@/components/ColorRadioPicker'
 import { colorIsDark, lighten, hexToRGB } from '@/utils/color'
 import { useCssVar } from '@vueuse/core'
 import { useAppStore } from '@/store/modules/app'
 import { trim, setCssVar } from '@/utils'
+import ColorRadioPicker from './components/ColorRadioPicker.vue'
 import InterfaceDisplay from './components/InterfaceDisplay.vue'
+import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
 
 const appStore = useAppStore()
 
@@ -26,6 +27,23 @@ const setSystemTheme = (color: string) => {
   setMenuTheme(trim(unref(leftMenuBgColor)))
 }
 
+// 头部主题相关
+const headerTheme = ref(appStore.getTheme.topHeaderBgColor)
+
+const setHeaderTheme = (color: string) => {
+  const isDarkColor = colorIsDark(color)
+  const textColor = isDarkColor ? '#fff' : 'inherit'
+  const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
+  setCssVar('--top-header-bg-color', color)
+  setCssVar('--top-header-text-color', textColor)
+  setCssVar('--top-header-hover-color', textHoverColor)
+  appStore.setTheme({
+    topHeaderBgColor: color,
+    topHeaderTextColor: textColor,
+    topHeaderHoverColor: textHoverColor
+  })
+}
+
 // 菜单主题相关
 const menuTheme = ref(appStore.getTheme.leftMenuBgColor)
 
@@ -81,6 +99,7 @@ const setMenuTheme = (color: string) => {
 
       <!-- 布局 -->
       <ElDivider>{{ t('setting.layout') }}</ElDivider>
+      <LayoutRadioPicker />
 
       <!-- 系统主题 -->
       <ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
@@ -99,6 +118,23 @@ const setMenuTheme = (color: string) => {
         @change="setSystemTheme"
       />
 
+      <!-- 头部主题 -->
+      <ElDivider>{{ t('setting.headerTheme') }}</ElDivider>
+      <ColorRadioPicker
+        v-model="headerTheme"
+        :schema="[
+          '#fff',
+          '#151515',
+          '#5172dc',
+          '#e74c3c',
+          '#24292e',
+          '#394664',
+          '#009688',
+          '#383f45'
+        ]"
+        @change="setHeaderTheme"
+      />
+
       <!-- 菜单主题 -->
       <ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
       <ColorRadioPicker

+ 0 - 0
src/components/ColorRadioPicker/src/ColorRadioPicker.vue → src/components/Setting/src/components/ColorRadioPicker.vue


+ 12 - 0
src/components/Setting/src/components/InterfaceDisplay.vue

@@ -71,6 +71,13 @@ const logoChange = (show: boolean) => {
   appStore.setLogo(show)
 }
 
+// 固定头部
+const fixedHeader = ref(appStore.getFixedHeader)
+
+const fixedHeaderChange = (show: boolean) => {
+  appStore.setFixedHeader(show)
+}
+
 // 灰色模式
 const greyMode = ref(appStore.getGreyMode)
 
@@ -126,6 +133,11 @@ const greyModeChange = (show: boolean) => {
       <ElSwitch v-model="logo" @change="logoChange" />
     </div>
 
+    <div class="flex justify-between items-center">
+      <span class="text-14px">{{ t('setting.fixedHeader') }}</span>
+      <ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" />
+    </div>
+
     <div class="flex justify-between items-center">
       <span class="text-14px">{{ t('setting.greyMode') }}</span>
       <ElSwitch v-model="greyMode" @change="greyModeChange" />

+ 98 - 0
src/components/Setting/src/components/LayoutRadioPicker.vue

@@ -0,0 +1,98 @@
+<script setup lang="ts">
+import { useAppStore } from '@/store/modules/app'
+import { computed } from 'vue'
+
+const appStore = useAppStore()
+
+const layout = computed(() => appStore.getLayout)
+console.log(layout.value)
+</script>
+
+<template>
+  <div class="v-layout-radio-picker flex flex-wrap space-x-14px">
+    <div
+      :class="[
+        'v-layout-radio-picker__classic relative w-56px h-48px cursor-pointer bg-gray-100',
+        {
+          'is-acitve': layout === 'classic'
+        }
+      ]"
+      @click="appStore.setLayout('classic')"
+    ></div>
+    <div
+      :class="[
+        'v-layout-radio-picker__top-left relative w-56px h-48px cursor-pointer bg-gray-100',
+        {
+          'is-acitve': layout === 'topLeft'
+        }
+      ]"
+      @click="appStore.setLayout('topLeft')"
+    ></div>
+  </div>
+</template>
+
+<style lang="less" scoped>
+@prefix-cls: ~'@{namespace}-layout-radio-picker';
+
+.@{prefix-cls} {
+  &__classic {
+    border: 2px solid #e5e7eb;
+    border-radius: 4px;
+
+    &:before {
+      position: absolute;
+      top: 0;
+      left: 0;
+      z-index: 1;
+      width: 33%;
+      height: 100%;
+      background-color: #273352;
+      border-radius: 4px 0 0 4px;
+      content: '';
+    }
+
+    &:after {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 25%;
+      background-color: #fff;
+      border-radius: 4px 4px 0 4px;
+      content: '';
+    }
+  }
+
+  &__top-left {
+    border: 2px solid #e5e7eb;
+    border-radius: 4px;
+
+    &:before {
+      position: absolute;
+      top: 0;
+      left: 0;
+      z-index: 1;
+      width: 100%;
+      height: 33%;
+      background-color: #273352;
+      border-radius: 4px 4px 0 0;
+      content: '';
+    }
+
+    &:after {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 33%;
+      height: 100%;
+      background-color: #fff;
+      border-radius: 4px 0 0 4px;
+      content: '';
+    }
+  }
+
+  .is-acitve {
+    border-color: var(--el-color-primary);
+  }
+}
+</style>

+ 7 - 6
src/components/SizeDropdown/src/SizeDropdown.vue

@@ -3,6 +3,12 @@ import { computed } from 'vue'
 import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
 import { useAppStore } from '@/store/modules/app'
 import { useI18n } from '@/hooks/web/useI18n'
+import { propTypes } from '@/utils/propTypes'
+
+defineProps({
+  color: propTypes.string.def('')
+})
+
 const { t } = useI18n()
 
 const appStore = useAppStore()
@@ -16,12 +22,7 @@ const setCurrentSize = (size: ElememtPlusSzie) => {
 
 <template>
   <ElDropdown trigger="click" @command="setCurrentSize">
-    <Icon
-      :size="18"
-      icon="mdi:format-size"
-      color="var(--el-text-color-primary)"
-      class="cursor-pointer"
-    />
+    <Icon :size="18" icon="mdi:format-size" :color="color" class="cursor-pointer" />
     <template #dropdown>
       <ElDropdownMenu>
         <ElDropdownItem v-for="item in sizeMap" :key="item" :command="item">

+ 9 - 5
src/components/TagsView/src/TagsView.vue

@@ -125,8 +125,10 @@ watch(
 </script>
 
 <template>
-  <div class="v-tags-view h-[var(--tags-view-height)] flex w-full">
-    <span class="v-tags-view__tool w-[40px] h-[40px] text-center leading-[40px] cursor-pointer">
+  <div class="v-tags-view h-[var(--tags-view-height)] flex w-full relative">
+    <span
+      class="v-tags-view__tool w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer"
+    >
       <Icon icon="ep:d-arrow-left" color="#333" />
     </span>
     <div class="overflow-hidden flex-1">
@@ -215,11 +217,13 @@ watch(
         </div>
       </ElScrollbar>
     </div>
-    <span class="v-tags-view__tool w-[40px] h-[40px] text-center leading-[40px] cursor-pointer">
+    <span
+      class="v-tags-view__tool w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer"
+    >
       <Icon icon="ep:d-arrow-right" color="#333" />
     </span>
     <span
-      class="v-tags-view__tool w-[40px] h-[40px] text-center leading-[40px] cursor-pointer"
+      class="v-tags-view__tool w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer"
       @click="refreshSelectedTag(selectedTag)"
     >
       <Icon icon="ant-design:reload-outlined" color="#333" />
@@ -275,7 +279,7 @@ watch(
       ]"
     >
       <span
-        class="v-tags-view__tool w-[40px] h-[40px] text-center leading-[40px] cursor-pointer block"
+        class="v-tags-view__tool w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer block"
       >
         <Icon icon="ant-design:setting-outlined" color="#333" />
       </span>

+ 1 - 1
src/components/UserInfo/src/UserInfo.vue

@@ -34,7 +34,7 @@ const loginOut = () => {
   <ElDropdown trigger="click">
     <div class="flex items-center">
       <img src="@/assets/imgs/avatar.png" alt="" class="w-[calc(var(--tags-view-height)-10px)]" />
-      <span class="<lg:hidden text-14px pl-[5px] text-dark-50">Archer</span>
+      <span class="<lg:hidden text-14px pl-[5px] text-[var(--top-header-text-color)]">Archer</span>
     </div>
     <template #dropdown>
       <ElDropdownMenu>

+ 11 - 3
src/config/app.ts

@@ -2,7 +2,7 @@ import { useCache } from '@/hooks/web/useCache'
 
 const { wsCache } = useCache()
 
-export type LayoutType = 'classic' | 'leftTop' | 'top' | 'test'
+export type LayoutType = 'classic' | 'topLeft' | 'leftTop' | 'top' | 'test'
 
 export interface AppState {
   breadcrumb: boolean
@@ -14,6 +14,7 @@ export interface AppState {
   locale: boolean
   tagsView: boolean
   logo: boolean
+  fixedHeader: boolean
   greyMode: boolean
 
   layout: LayoutType
@@ -37,9 +38,10 @@ export const appModules: AppState = {
   locale: true, // 多语言图标
   tagsView: true, // 标签页
   logo: true, // logo
+  fixedHeader: true, // 固定toolheader
   greyMode: false, // 是否开始灰色模式,用于特殊悼念日
 
-  layout: 'classic', // layout布局
+  layout: wsCache.get('layout') || 'classic', // layout布局
   title: 'butterfly-admin', // 标题
   logoTitle: 'ButterflyAdmin', // logo标题
   userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
@@ -67,6 +69,12 @@ export const appModules: AppState = {
     // logo字体颜色
     logoTitleTextColor: '#fff',
     // logo边框颜色
-    logoBorderColor: 'inherit'
+    logoBorderColor: 'inherit',
+    // 头部背景颜色
+    topHeaderBgColor: '#fff',
+    // 头部字体颜色
+    topHeaderTextColor: 'inherit',
+    // 头部悬停颜色
+    topHeaderHoverColor: '#f6f6f6'
   }
 }

+ 18 - 100
src/layout/Layout.vue

@@ -1,100 +1,50 @@
 <script lang="tsx">
-import { computed, defineComponent } from 'vue'
+import { computed, defineComponent, unref } from 'vue'
 import { useAppStore } from '@/store/modules/app'
-import { Menu } from '@/components/Menu'
-import { Collapse } from '@/components/Collapse'
-import { LocaleDropdown } from '@/components/LocaleDropdown'
-import { SizeDropdown } from '@/components/SizeDropdown'
-import { UserInfo } from '@/components/UserInfo'
-import { Screenfull } from '@/components/Screenfull'
-import { Breadcrumb } from '@/components/Breadcrumb'
-import { TagsView } from '@/components/TagsView'
 import { Backtop } from '@/components/Backtop'
 import { Setting } from '@/components/Setting'
-import AppView from './components/AppView.vue'
+import { useRenderLayout } from './components/useRenderLayout'
 
 const appStore = useAppStore()
 
 // 是否是移动端
 const mobile = computed(() => appStore.getMobile)
 
-// 面包屑
-const breadcrumb = computed(() => appStore.getBreadcrumb)
-
 // 菜单折叠
 const collapse = computed(() => appStore.getCollapse)
 
-// 折叠图标
-const hamburger = computed(() => appStore.getHamburger)
-
-// 全屏图标
-const screenfull = computed(() => appStore.getScreenfull)
-
-// 尺寸图标
-const size = computed(() => appStore.getSize)
-
-// 多语言图标
-const locale = computed(() => appStore.getLocale)
-
-// 标签页
-const tagsView = computed(() => appStore.getTagsView)
-
-const classSuffix = computed(() => appStore.getLayout)
+const layout = computed(() => appStore.getLayout)
 
 const handleClickOutside = () => {
   appStore.setCollapse(true)
 }
 
+const renderLayout = () => {
+  switch (unref(layout)) {
+    case 'classic':
+      const { renderClassic } = useRenderLayout()
+      return renderClassic()
+    case 'topLeft':
+      const { renderTopLeft } = useRenderLayout()
+      return renderTopLeft()
+    default:
+      break
+  }
+}
+
 export default defineComponent({
   name: 'Layout',
   setup() {
     return () => (
-      <section class={['v-app', `v-app__${classSuffix.value}`, 'w-[100%] h-[100%] relative']}>
+      <section class={['v-app', `v-app__${layout.value}`, 'w-[100%] h-[100%] relative']}>
         {mobile.value && !collapse.value ? (
           <div
             class="absolute top-0 left-0 w-full h-full opacity-30 z-99 bg-[var(--el-color-black)]"
             onClick={handleClickOutside}
           ></div>
         ) : undefined}
-        <Menu class="absolute top-0 left-0"></Menu>
-        <div
-          class={[
-            'v-app-right',
-            'absolute top-0 h-[100%]',
-            collapse.value
-              ? 'w-[calc(100%-var(--left-menu-min-width))]'
-              : 'w-[calc(100%-var(--left-menu-max-width))]',
-            collapse.value
-              ? 'left-[var(--left-menu-min-width)]'
-              : 'left-[var(--left-menu-max-width)]',
-            '<md:(!left-0 !w-[100%])'
-          ]}
-        >
-          <div
-            class={[
-              'v-app-right__tool',
-              'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between'
-            ]}
-          >
-            <div class="h-full flex items-center">
-              {hamburger.value ? <Collapse class="header__tigger"></Collapse> : undefined}
-              {breadcrumb.value ? <Breadcrumb class="<md:hidden"></Breadcrumb> : undefined}
-            </div>
-            <div class="h-full flex items-center">
-              {screenfull.value ? <Screenfull class="header__tigger"></Screenfull> : undefined}
-              {size.value ? <SizeDropdown class="header__tigger"></SizeDropdown> : undefined}
-              {locale.value ? <LocaleDropdown class="header__tigger"></LocaleDropdown> : undefined}
-              <UserInfo class="header__tigger"></UserInfo>
-            </div>
-          </div>
-          {tagsView.value ? (
-            <div class="v-app-right__tags-view relative">
-              <TagsView></TagsView>
-            </div>
-          ) : undefined}
 
-          <AppView></AppView>
-        </div>
+        {renderLayout()}
 
         <Backtop></Backtop>
 
@@ -107,36 +57,4 @@ export default defineComponent({
 
 <style lang="less" scoped>
 @prefix-cls: ~'@{namespace}-app';
-
-.header__tigger {
-  display: flex;
-  height: 100%;
-  padding: 1px 10px 0;
-  cursor: pointer;
-  align-items: center;
-  transition: background var(--transition-time-02);
-
-  &:hover {
-    background-color: #f6f6f6;
-  }
-}
-
-.@{prefix-cls} {
-  &-right {
-    transition: left var(--transition-time-02);
-
-    &__tool,
-    &__tags-view {
-      &::after {
-        position: absolute;
-        bottom: 0;
-        left: 0;
-        width: 100%;
-        height: 1px;
-        border-top: 1px solid var(--top-tool-border-color);
-        content: '';
-      }
-    }
-  }
-}
 </style>

+ 9 - 18
src/layout/components/AppView.vue

@@ -10,22 +10,13 @@ const getCaches = computed((): string[] => {
 </script>
 
 <template>
-  <el-scrollbar
-    :class="[
-      'v-content',
-      {
-        '!h-[calc(100%-var(--top-tool-height)-var(--tags-view-height))]': true
-      }
-    ]"
-  >
-    <section class="p-[var(--app-content-padding)]">
-      <router-view>
-        <template #default="{ Component, route }">
-          <keep-alive :include="getCaches">
-            <component :is="Component" :key="route.fullPath" />
-          </keep-alive>
-        </template>
-      </router-view>
-    </section>
-  </el-scrollbar>
+  <section class="p-[var(--app-content-padding)] w-[100%]">
+    <router-view>
+      <template #default="{ Component, route }">
+        <keep-alive :include="getCaches">
+          <component :is="Component" :key="route.fullPath" />
+        </keep-alive>
+      </template>
+    </router-view>
+  </section>
 </template>

+ 71 - 0
src/layout/components/ToolHeader.vue

@@ -0,0 +1,71 @@
+<script lang="tsx">
+import { defineComponent, computed } from 'vue'
+import { Collapse } from '@/components/Collapse'
+import { LocaleDropdown } from '@/components/LocaleDropdown'
+import { SizeDropdown } from '@/components/SizeDropdown'
+import { UserInfo } from '@/components/UserInfo'
+import { Screenfull } from '@/components/Screenfull'
+import { Breadcrumb } from '@/components/Breadcrumb'
+import { useAppStore } from '@/store/modules/app'
+
+const appStore = useAppStore()
+
+// 面包屑
+const breadcrumb = computed(() => appStore.getBreadcrumb)
+
+// 折叠图标
+const hamburger = computed(() => appStore.getHamburger)
+
+// 全屏图标
+const screenfull = computed(() => appStore.getScreenfull)
+
+// 尺寸图标
+const size = computed(() => appStore.getSize)
+
+// 多语言图标
+const locale = computed(() => appStore.getLocale)
+
+export default defineComponent({
+  name: 'ToolHeader',
+  setup() {
+    return () => (
+      <div
+        class={[
+          'v-tool-header',
+          'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between'
+        ]}
+      >
+        <div class="h-full flex items-center">
+          {hamburger.value ? (
+            <Collapse class="hover-tigger" color="var(--top-header-text-color)"></Collapse>
+          ) : undefined}
+          {breadcrumb.value ? <Breadcrumb class="<md:hidden"></Breadcrumb> : undefined}
+        </div>
+        <div class="h-full flex items-center">
+          {screenfull.value ? (
+            <Screenfull class="hover-tigger" color="var(--top-header-text-color)"></Screenfull>
+          ) : undefined}
+          {size.value ? (
+            <SizeDropdown class="hover-tigger" color="var(--top-header-text-color)"></SizeDropdown>
+          ) : undefined}
+          {locale.value ? (
+            <LocaleDropdown
+              class="hover-tigger"
+              color="var(--top-header-text-color)"
+            ></LocaleDropdown>
+          ) : undefined}
+          <UserInfo class="hover-tigger"></UserInfo>
+        </div>
+      </div>
+    )
+  }
+})
+</script>
+
+<style lang="less" scoped>
+@prefix-cls: ~'@{namespace}-tool-header';
+
+.@{prefix-cls} {
+  transition: left var(--transition-time-02);
+}
+</style>

+ 155 - 0
src/layout/components/useRenderLayout.tsx

@@ -0,0 +1,155 @@
+import { computed } from 'vue'
+import { useAppStore } from '@/store/modules/app'
+import { Menu } from '@/components/Menu'
+import { TagsView } from '@/components/TagsView'
+import { Logo } from '@/components/Logo'
+import AppView from './AppView.vue'
+import ToolHeader from './ToolHeader.vue'
+import { ElScrollbar } from 'element-plus'
+
+const appStore = useAppStore()
+
+// 标签页
+const tagsView = computed(() => appStore.getTagsView)
+
+// 菜单折叠
+const collapse = computed(() => appStore.getCollapse)
+
+// logo
+const logo = computed(() => appStore.logo)
+
+// 固定头部
+const fixedHeader = computed(() => appStore.getFixedHeader)
+
+// 是否是移动端
+const mobile = computed(() => appStore.getMobile)
+
+export const useRenderLayout = () => {
+  const renderClassic = () => {
+    return (
+      <>
+        <div class={['absolute top-0 left-0 h-full', { '!fixed z-99': mobile.value }]}>
+          {logo.value ? (
+            <Logo
+              class={[
+                'bg-[var(--left-menu-bg-color)]',
+                {
+                  '!pl-0': mobile.value && collapse.value,
+                  'w-[var(--left-menu-min-width)]': appStore.getCollapse,
+                  'w-[var(--left-menu-max-width)]': !appStore.getCollapse
+                }
+              ]}
+              style="transition: all var(--transition-time-02);"
+            ></Logo>
+          ) : undefined}
+          <Menu class={[{ '!h-[calc(100%-var(--logo-height))]': logo.value }]}></Menu>
+        </div>
+        <div
+          class={[
+            'v-app-right',
+            'absolute top-0 h-[100%]',
+            {
+              'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
+                collapse.value && !mobile.value && !mobile.value,
+              'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
+                !collapse.value && !mobile.value && !mobile.value,
+              'fixed !w-full !left-0': mobile.value
+            }
+          ]}
+          style="transition: all var(--transition-time-02);"
+        >
+          <ElScrollbar
+            class={[
+              'v-content',
+              {
+                '!h-[calc(100%-var(--top-tool-height)-var(--tags-view-height))] mt-[calc(var(--top-tool-height)+var(--tags-view-height))]':
+                  fixedHeader.value
+              }
+            ]}
+          >
+            <div
+              class={[
+                {
+                  'fixed top-0 left-0 z-10': fixedHeader.value,
+                  'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
+                    collapse.value && fixedHeader.value && !mobile.value,
+                  'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
+                    !collapse.value && fixedHeader.value && !mobile.value,
+                  '!w-full !left-0': mobile.value
+                }
+              ]}
+              style="transition: all var(--transition-time-02);"
+            >
+              <ToolHeader class="border-bottom bg-[var(--top-header-bg-color)]"></ToolHeader>
+
+              {tagsView.value ? <TagsView class="border-bottom"></TagsView> : undefined}
+            </div>
+
+            <AppView></AppView>
+          </ElScrollbar>
+        </div>
+      </>
+    )
+  }
+
+  const renderTopLeft = () => {
+    return (
+      <>
+        <div class="flex items-center bg-[var(--top-header-bg-color)]">
+          <Logo class="hover-tigger !pr-15px"></Logo>
+
+          <ToolHeader class="flex-1"></ToolHeader>
+        </div>
+        <div class="absolute top-[var(--logo-height)] left-0 w-full h-[calc(100%-var(--logo-height))] flex">
+          <Menu class="!h-full"></Menu>
+          <div
+            class={[
+              'v-app-right',
+              'h-[100%]',
+              {
+                'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
+                  collapse.value,
+                'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
+                  !collapse.value
+              }
+            ]}
+            style="transition: all var(--transition-time-02);"
+          >
+            <ElScrollbar
+              class={[
+                'v-content',
+                {
+                  '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
+                    fixedHeader.value && tagsView.value
+                }
+              ]}
+            >
+              {tagsView.value ? (
+                <TagsView
+                  class={[
+                    'border-bottom border-top',
+                    {
+                      '!fixed top-0 left-0 z-10': fixedHeader.value,
+                      'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)] mt-[var(--logo-height)]':
+                        collapse.value && fixedHeader.value,
+                      'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)] mt-[var(--logo-height)]':
+                        !collapse.value && fixedHeader.value
+                    }
+                  ]}
+                  style="transition: all var(--transition-time-02);"
+                ></TagsView>
+              ) : undefined}
+
+              <AppView></AppView>
+            </ElScrollbar>
+          </div>
+        </div>
+      </>
+    )
+  }
+
+  return {
+    renderClassic,
+    renderTopLeft
+  }
+}

+ 3 - 1
src/locales/en.ts

@@ -35,7 +35,9 @@ export default {
     localeIcon: 'Locale icon',
     tagsView: 'Tags view',
     logo: 'Logo',
-    greyMode: 'Grey mode'
+    greyMode: 'Grey mode',
+    fixedHeader: 'Fixed header',
+    headerTheme: 'Header theme'
   },
   size: {
     default: 'Default',

+ 3 - 1
src/locales/zh-CN.ts

@@ -35,7 +35,9 @@ export default {
     localeIcon: '多语言图标',
     tagsView: '标签页',
     logo: '标志',
-    greyMode: '灰色模式'
+    greyMode: '灰色模式',
+    fixedHeader: '固定头部',
+    headerTheme: '头部主题'
   },
   size: {
     default: '默认',

+ 12 - 0
src/store/modules/app.ts

@@ -4,6 +4,7 @@ import { useCache } from '@/hooks/web/useCache'
 import { appModules } from '@/config/app'
 import type { AppState, LayoutType } from '@/config/app'
 import { setCssVar, humpToUnderline } from '@/utils'
+import { ElMessage } from 'element-plus'
 
 const { wsCache } = useCache()
 
@@ -38,6 +39,9 @@ export const useAppStore = defineStore({
     getLogo(): boolean {
       return this.logo
     },
+    getFixedHeader(): boolean {
+      return this.fixedHeader
+    },
     getGreyMode(): boolean {
       return this.greyMode
     },
@@ -98,12 +102,20 @@ export const useAppStore = defineStore({
     setLogo(logo: boolean) {
       this.logo = logo
     },
+    setFixedHeader(fixedHeader: boolean) {
+      this.fixedHeader = fixedHeader
+    },
     setGreyMode(greyMode: boolean) {
       this.greyMode = greyMode
     },
 
     setLayout(layout: LayoutType) {
+      if (this.mobile && layout !== 'classic') {
+        ElMessage.warning('移动端模式下不支持切换其他布局')
+        return
+      }
       this.layout = layout
+      wsCache.set('layout', this.layout)
     },
     setTitle(title: string) {
       this.title = title

+ 25 - 0
src/styles/common.less

@@ -0,0 +1,25 @@
+.hover-tigger {
+  @apply flex h-full pt-1px px-10px cursor-pointer items-center;
+  transition: background var(--transition-time-02);
+  &:hover {
+    background-color: var(--top-header-hover-color);
+  }
+}
+
+.border-bottom {
+  @apply relative;
+  &:after {
+    content: '';
+    border-top: 1px solid var(--top-tool-border-color);
+    @apply absolute bottom-0 left-0 w-full h-1px;
+  }
+}
+
+.border-top {
+  @apply relative;
+  &:before {
+    content: '';
+    border-top: 1px solid var(--top-tool-border-color);
+    @apply absolute top-0 left-0 w-full h-1px;
+  }
+}

+ 1 - 0
src/styles/index.less

@@ -1 +1,2 @@
 @import './var.css';
+@import './common.less';

+ 7 - 1
src/styles/var.css

@@ -30,13 +30,19 @@
   /* logo end */
 
   /* header start */
+  --top-header-bg-color: '#fff';
+
+  --top-header-text-color: 'inherit';
+
+  --top-header-hover-color: #f6f6f6;
+
   --top-tool-height: var(--logo-height);
 
   --top-tool-p-x: 0;
 
   --top-tool-border-color: #eee;
 
-  --tags-view-height: 40px;
+  --tags-view-height: 35px;
   /* header start */
 
   --app-content-padding: 20px;

+ 2 - 1
src/views/Login/components/LoginForm.vue

@@ -132,7 +132,8 @@ const signIn = async () => {
         addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
       })
       permissionStore.setIsAddRouters(true)
-      push({ path: redirect.value || permissionStore.addRouters[0].path })
+      // push({ path: redirect.value || permissionStore.addRouters[0].path })
+      push({ path: permissionStore.addRouters[0].path })
     }
   }
 }

+ 2 - 1
vite.config.ts

@@ -117,7 +117,8 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
       },
       hmr: {
         overlay: false
-      }
+      },
+      host: '0.0.0.0'
     },
     optimizeDeps: {
       include: [

+ 28 - 10
windi.config.ts

@@ -1,4 +1,5 @@
 import { defineConfig } from 'windicss/helpers'
+// import plugin from 'windicss/plugin'
 
 // function range(size, startAt = 1) {
 //   return Array.from(Array(size).keys()).map((i) => i + startAt)
@@ -34,16 +35,33 @@ export default defineConfig({
     //   // ...range(50).map((i) => `ml-${i}px`)
     // }
   }
-  // Plugin: [
-  //   require('@windicss/plugin-animations')({
-  //     settings: {
-  //       animatedSpeed: 1000,
-  //       heartBeatSpeed: 1000,
-  //       hingeSpeed: 2000,
-  //       bounceInSpeed: 750,
-  //       bounceOutSpeed: 750,
-  //       animationDelaySpeed: 1000
-  //     }
+  // plugins: [
+  //   plugin(({ addComponents }) => {
+  //     addComponents({
+  //       '.hover-tigger': {
+  //         display: 'flex',
+  //         height: '100%',
+  //         padding: '1px 10px 0',
+  //         cursor: 'pointer',
+  //         alignItems: 'center',
+  //         transition: 'background var(--transition-time-02)',
+  //         '&:hover': {
+  //           backgroundColor: '#f6f6f6'
+  //         }
+  //       },
+  //       '.border-bottom': {
+  //         position: 'relative',
+  //         '&:after': {
+  //           position: 'absolute',
+  //           bottom: '0',
+  //           left: '0',
+  //           width: '100%',
+  //           height: '1px',
+  //           borderTop: '1px solid var(--top-tool-border-color)',
+  //           content: ''
+  //         }
+  //       }
+  //     })
   //   })
   // ]
 })