Преглед на файлове

feat: add mix nav (#187)

feat: add mix nav
啝裳 преди 3 години
родител
ревизия
ef05b2b614

+ 3 - 3
README.en-US.md

@@ -126,13 +126,13 @@ If you think this project is helpful to you, you can help the author buy a cup o
 
 ## License
 
-In principle, no fees and copyrights are charged, so you can use it with confidence
+In principle, no fees and copyrights are charged, and you can use it with confidence, but if you need secondary open source, please contact the author for permission!
 
 [MIT © xiaoxian521-2020](./LICENSE)
 
 ## Backers
 
-Thank you very much for your support, I believe the project will get better and better! ! ! :heart:
+Thank you very much for your support, I believe the project will get better and better :heart:
 
 |                                                                xueyuheng                                                                 |                                                                taolei1990                                                                 |                                                                hang-kim                                                                 |                                                               madwolfcrazy                                                                |                                                                limuen                                                                 |
 | :--------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: |
@@ -140,6 +140,6 @@ Thank you very much for your support, I believe the project will get better and
 
 ## Contributors
 
-This project exists thanks to all the people who contribute!!! :heart:
+This project exists thanks to all the people who contribute :heart:
 
 <a href="https://github.com/xiaoxian521/vue-pure-admin/graphs/contributors"><img src="https://contrib.rocks/image?repo=xiaoxian521/vue-pure-admin" /></a>

+ 3 - 3
README.md

@@ -132,13 +132,13 @@ pnpm build
 
 ## 许可证
 
-原则上不收取任何费用及版权,可以放心使用
+原则上不收取任何费用及版权,可以放心使用,不过如需二次开源(比如用此平台二次开发并开源)请联系作者获取许可!
 
 [MIT © xiaoxian521-2020](./LICENSE)
 
 ## 捐赠者
 
-非常感谢你们的支持,相信项目会越来越好!!!:heart:
+非常感谢你们的支持,相信项目会越来越好 :heart:
 
 |                                                                xueyuheng                                                                 |                                                                taolei1990                                                                 |                                                                hang-kim                                                                 |                                                               madwolfcrazy                                                                |                                                                limuen                                                                 |
 | :--------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------: |
@@ -146,6 +146,6 @@ pnpm build
 
 ## 贡献者
 
-这个项目的存在感谢所有做出贡献的人!!!:heart:
+这个项目的存在感谢所有做出贡献的人 :heart:
 
 <a href="https://github.com/xiaoxian521/vue-pure-admin/graphs/contributors"><img src="https://contrib.rocks/image?repo=xiaoxian521/vue-pure-admin" /></a>

+ 12 - 12
package.json

@@ -33,17 +33,17 @@
     "@ctrl/tinycolor": "^3.4.0",
     "@logicflow/core": "0.7.1",
     "@logicflow/extension": "0.7.1",
-    "@vueuse/core": "^7.5.5",
+    "@vueuse/core": "^7.6.2",
     "@vueuse/motion": "^2.0.0-beta.9",
-    "@vueuse/shared": "^7.5.5",
+    "@vueuse/shared": "^7.6.2",
     "animate.css": "^4.1.1",
     "axios": "^0.25.0",
     "cropperjs": "^1.5.11",
     "css-color-function": "^1.3.3",
     "dayjs": "^1.10.7",
     "driver.js": "^0.9.8",
-    "echarts": "^5.2.1",
-    "element-plus": "^2.0.0",
+    "echarts": "^5.3.0",
+    "element-plus": "^2.0.2",
     "element-resize-detector": "^1.2.3",
     "js-cookie": "^3.0.1",
     "lodash-es": "^4.17.21",
@@ -57,7 +57,7 @@
     "responsive-storage": "^1.0.11",
     "rgb-hex": "^4.0.0",
     "v-contextmenu": "3.0.0",
-    "vue": "^3.2.29",
+    "vue": "^3.2.31",
     "vue-i18n": "^9.2.0-beta.30",
     "vue-json-pretty": "^2.0.2",
     "vue-router": "^4.0.12",
@@ -84,9 +84,9 @@
     "@types/qs": "^6.9.7",
     "@typescript-eslint/eslint-plugin": "^5.10.2",
     "@typescript-eslint/parser": "^5.10.2",
-    "@vitejs/plugin-legacy": "^1.6.4",
-    "@vitejs/plugin-vue": "^2.1.0",
-    "@vitejs/plugin-vue-jsx": "^1.3.3",
+    "@vitejs/plugin-legacy": "^1.7.0",
+    "@vitejs/plugin-vue": "^2.2.0",
+    "@vitejs/plugin-vue-jsx": "^1.3.4",
     "@vue/eslint-config-prettier": "^7.0.0",
     "@vue/eslint-config-typescript": "^10.0.0",
     "@zougt/vite-plugin-theme-preprocessor": "^1.4.4",
@@ -97,7 +97,7 @@
     "eslint-plugin-vue": "^8.4.1",
     "husky": "^7.0.4",
     "lint-staged": "11.1.2",
-    "postcss": "8.2.6",
+    "postcss": "^8.4.6",
     "postcss-html": "^1.3.0",
     "postcss-import": "14.0.0",
     "postcss-scss": "^4.0.3",
@@ -115,12 +115,12 @@
     "stylelint-order": "^5.0.0",
     "typescript": "^4.5.5",
     "unplugin-element-plus": "^0.2.0",
-    "vite": "^2.7.13",
+    "vite": "^2.8.1",
     "vite-plugin-live-reload": "^2.1.0",
     "vite-plugin-mock": "^2.9.6",
     "vite-plugin-remove-console": "^0.0.6",
-    "vite-plugin-style-import": "^1.4.1",
-    "vite-plugin-windicss": "^1.6.1",
+    "vite-plugin-style-import": "1.4.1",
+    "vite-plugin-windicss": "^1.7.0",
     "vite-svg-loader": "2.2.0",
     "vue-eslint-parser": "^8.2.0",
     "windicss": "^3.4.3"

Файловите разлики са ограничени, защото са твърде много
+ 238 - 223
pnpm-lock.yaml


+ 23 - 48
src/layout/components/navbar.vue

@@ -1,71 +1,43 @@
 <script setup lang="ts">
-import { computed } from "vue";
 import { useI18n } from "vue-i18n";
-import { emitter } from "/@/utils/mitt";
+import { useNav } from "../hooks/nav";
+import { useRoute } from "vue-router";
 import Notice from "./notice/index.vue";
+import mixNav from "./sidebar/mixNav.vue";
 import avatars from "/@/assets/avatars.jpg";
-import { transformI18n } from "/@/plugins/i18n";
 import Hamburger from "./sidebar/hamBurger.vue";
-import { useRouter, useRoute } from "vue-router";
-import { storageSession } from "/@/utils/storage";
+import { watch, getCurrentInstance } from "vue";
 import Breadcrumb from "./sidebar/breadCrumb.vue";
-import { useAppStoreHook } from "/@/store/modules/app";
-import { unref, watch, getCurrentInstance } from "vue";
 import { deviceDetection } from "/@/utils/deviceDetection";
 import screenfull from "../components/screenfull/index.vue";
-import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
 import globalization from "/@/assets/svg/globalization.svg?component";
 
-const instance =
-  getCurrentInstance().appContext.config.globalProperties.$storage;
-const pureApp = useAppStoreHook();
-const router = useRouter();
 const route = useRoute();
-let usename = storageSession.getItem("info")?.username;
 const { locale } = useI18n();
-
-const getDropdownItemStyle = computed(() => {
-  return t => {
-    return {
-      background: locale.value === t ? useEpThemeStoreHook().epThemeColor : "",
-      color: locale.value === t ? "#f4f4f5" : "#000"
-    };
-  };
-});
+const instance =
+  getCurrentInstance().appContext.config.globalProperties.$storage;
+const {
+  logout,
+  onPanel,
+  changeTitle,
+  toggleSideBar,
+  pureApp,
+  usename,
+  getDropdownItemStyle
+} = useNav();
 
 watch(
   () => locale.value,
   () => {
-    //@ts-ignore
-    document.title = transformI18n(
-      //@ts-ignore
-      unref(route.meta.title),
-      unref(route.meta.i18n)
-    ); // 动态title
+    changeTitle(route.meta);
   }
 );
 
-// 退出登录
-const logout = (): void => {
-  storageSession.removeItem("info");
-  router.push("/login");
-};
-
-function onPanel() {
-  emitter.emit("openPanel");
-}
-
-function toggleSideBar() {
-  pureApp.toggleSideBar();
-}
-
-// 简体中文
 function translationCh() {
   instance.locale = { locale: "zh" };
   locale.value = "zh";
 }
 
-// English
 function translationEn() {
   instance.locale = { locale: "en" };
   locale.value = "en";
@@ -75,14 +47,17 @@ function translationEn() {
 <template>
   <div class="navbar">
     <Hamburger
+      v-if="pureApp.layout !== 'mix'"
       :is-active="pureApp.sidebar.opened"
       class="hamburger-container"
       @toggleClick="toggleSideBar"
     />
 
-    <Breadcrumb class="breadcrumb-container" />
+    <Breadcrumb v-if="pureApp.layout !== 'mix'" class="breadcrumb-container" />
+
+    <mixNav v-if="pureApp.layout === 'mix'" />
 
-    <div class="vertical-header-right">
+    <div v-if="pureApp.layout === 'vertical'" class="vertical-header-right">
       <!-- 通知 -->
       <Notice id="header-notice" />
       <!-- 全屏 -->
@@ -93,7 +68,7 @@ function translationEn() {
         <template #dropdown>
           <el-dropdown-menu class="translation">
             <el-dropdown-item
-              :style="getDropdownItemStyle('zh')"
+              :style="getDropdownItemStyle(locale, 'zh')"
               @click="translationCh"
               ><IconifyIconOffline
                 class="check-zh"
@@ -102,7 +77,7 @@ function translationEn() {
               />简体中文</el-dropdown-item
             >
             <el-dropdown-item
-              :style="getDropdownItemStyle('en')"
+              :style="getDropdownItemStyle(locale, 'en')"
               @click="translationEn"
               ><el-icon class="check-en" v-show="locale === 'en'"
                 ><IconifyIconOffline icon="check" /></el-icon

+ 60 - 23
src/layout/components/setting/index.vue

@@ -61,6 +61,7 @@ let themeColors = ref<Array<themeColorsType>>([
 
 const verticalRef = templateRef<HTMLElement | null>("verticalRef", null);
 const horizontalRef = templateRef<HTMLElement | null>("horizontalRef", null);
+const mixRef = templateRef<HTMLElement | null>("mixRef", null);
 
 let layoutTheme =
   ref(storageLocal.getItem("responsive-layout")) ||
@@ -101,7 +102,7 @@ const getThemeColorStyle = computed(() => {
   };
 });
 
-function changeStorageConfigure(key, val) {
+function storageConfigureChange<T>(key: string, val: T): void {
   const storageConfigure = instance.configure;
   storageConfigure[key] = val;
   instance.configure = storageConfigure;
@@ -117,7 +118,7 @@ function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
 // 灰色模式设置
 const greyChange = (value): void => {
   toggleClass(settings.greyVal, "html-grey", document.querySelector("html"));
-  changeStorageConfigure("grey", value);
+  storageConfigureChange("grey", value);
 };
 
 // 色弱模式设置
@@ -127,29 +128,30 @@ const weekChange = (value): void => {
     "html-weakness",
     document.querySelector("html")
   );
-  changeStorageConfigure("weak", value);
+  storageConfigureChange("weak", value);
 };
 
 const tagsChange = () => {
   let showVal = settings.tabsVal;
-  changeStorageConfigure("hideTabs", showVal);
+  storageConfigureChange("hideTabs", showVal);
   emitter.emit("tagViewsChange", showVal);
 };
 
 const multiTagsCacheChange = () => {
   let multiTagsCache = settings.multiTagsCache;
-  changeStorageConfigure("multiTagsCache", multiTagsCache);
+  storageConfigureChange("multiTagsCache", multiTagsCache);
   useMultiTagsStoreHook().multiTagsCacheChange(multiTagsCache);
 };
 
 // 清空缓存并返回登录页
 function onReset() {
-  toggleClass(getConfig().Grey, "html-grey", document.querySelector("html"));
-  toggleClass(
-    getConfig().Weak,
-    "html-weakness",
-    document.querySelector("html")
-  );
+  router.push("/login");
+  const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
+  useAppStoreHook().setLayout(Layout);
+  useEpThemeStoreHook().setEpThemeColor(EpThemeColor);
+  useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
+  toggleClass(Grey, "html-grey", document.querySelector("html"));
+  toggleClass(Weak, "html-weakness", document.querySelector("html"));
   useMultiTagsStoreHook().handleTags("equal", [
     {
       path: "/welcome",
@@ -161,23 +163,20 @@ function onReset() {
       }
     }
   ]);
-  useMultiTagsStoreHook().multiTagsCacheChange(getConfig().MultiTagsCache);
-  useEpThemeStoreHook().setEpThemeColor(getConfig().EpThemeColor);
   storageLocal.clear();
   storageSession.clear();
-  router.push("/login");
 }
 
 function onChange(label) {
-  changeStorageConfigure("showModel", label);
+  storageConfigureChange("showModel", label);
   emitter.emit("tagViewsShowModel", label);
 }
 
 // 侧边栏Logo
 function logoChange() {
   unref(logoVal)
-    ? changeStorageConfigure("showLogo", true)
-    : changeStorageConfigure("showLogo", false);
+    ? storageConfigureChange("showLogo", true)
+    : storageConfigureChange("showLogo", false);
   emitter.emit("logoChange", unref(logoVal));
 }
 
@@ -192,10 +191,17 @@ watch(instance, ({ layout }) => {
     case "vertical":
       toggleClass(true, isSelect, unref(verticalRef));
       debounce(setFalse([horizontalRef]), 50);
+      debounce(setFalse([mixRef]), 50);
       break;
     case "horizontal":
       toggleClass(true, isSelect, unref(horizontalRef));
       debounce(setFalse([verticalRef]), 50);
+      debounce(setFalse([mixRef]), 50);
+      break;
+    case "mix":
+      toggleClass(true, isSelect, unref(mixRef));
+      debounce(setFalse([verticalRef]), 50);
+      debounce(setFalse([horizontalRef]), 50);
       break;
   }
 });
@@ -315,7 +321,7 @@ nextTick(() => {
 
     <el-divider>导航栏模式</el-divider>
     <ul class="pure-theme">
-      <el-tooltip class="item" content="左侧菜单模式" placement="bottom">
+      <el-tooltip class="item" content="左侧模式" placement="bottom">
         <li
           :class="layoutTheme.layout === 'vertical' ? $style.isSelect : ''"
           ref="verticalRef"
@@ -326,7 +332,7 @@ nextTick(() => {
         </li>
       </el-tooltip>
 
-      <el-tooltip class="item" content="顶部菜单模式" placement="bottom">
+      <el-tooltip class="item" content="顶部模式" placement="bottom">
         <li
           :class="layoutTheme.layout === 'horizontal' ? $style.isSelect : ''"
           ref="horizontalRef"
@@ -336,6 +342,17 @@ nextTick(() => {
           <div></div>
         </li>
       </el-tooltip>
+
+      <el-tooltip class="item" content="混合模式" placement="bottom">
+        <li
+          :class="layoutTheme.layout === 'mix' ? $style.isSelect : ''"
+          ref="mixRef"
+          @click="setLayoutModel('mix')"
+        >
+          <div></div>
+          <div></div>
+        </li>
+      </el-tooltip>
     </ul>
 
     <el-divider v-show="!dataTheme">主题色</el-divider>
@@ -481,15 +498,14 @@ nextTick(() => {
 .pure-theme {
   margin-top: 25px;
   width: 100%;
-  height: 100px;
+  height: 50px;
   display: flex;
   flex-wrap: wrap;
   justify-content: space-around;
 
   li {
-    margin: 10px;
-    width: 36%;
-    height: 70px;
+    width: 18%;
+    height: 45px;
     background: #f0f2f5;
     position: relative;
     overflow: hidden;
@@ -527,6 +543,27 @@ nextTick(() => {
         }
       }
     }
+
+    &:nth-child(3) {
+      div {
+        &:nth-child(1) {
+          width: 100%;
+          height: 30%;
+          background: #1b2a47;
+          box-shadow: 0 0 1px #888;
+        }
+
+        &:nth-child(2) {
+          width: 30%;
+          height: 70%;
+          bottom: 0;
+          left: 0;
+          background: #fff;
+          box-shadow: 0 0 1px #888;
+          position: absolute;
+        }
+      }
+    }
   }
 }
 

+ 28 - 94
src/layout/components/sidebar/horizontal.vue

@@ -1,126 +1,61 @@
 <script setup lang="ts">
-import {
-  computed,
-  unref,
-  watch,
-  nextTick,
-  onMounted,
-  getCurrentInstance
-} from "vue";
 import { useI18n } from "vue-i18n";
-import { emitter } from "/@/utils/mitt";
+import { useNav } from "../../hooks/nav";
 import Notice from "../notice/index.vue";
 import { templateRef } from "@vueuse/core";
 import SidebarItem from "./sidebarItem.vue";
 import avatars from "/@/assets/avatars.jpg";
 import screenfull from "../screenfull/index.vue";
 import { useRoute, useRouter } from "vue-router";
-import { storageSession } from "/@/utils/storage";
 import { deviceDetection } from "/@/utils/deviceDetection";
+import { watch, nextTick, onMounted, getCurrentInstance } from "vue";
 import { usePermissionStoreHook } from "/@/store/modules/permission";
 import globalization from "/@/assets/svg/globalization.svg?component";
 
+const route = useRoute();
+const { locale } = useI18n();
+const routers = useRouter().options.routes;
+const menuRef = templateRef<ElRef | null>("menu", null);
 const instance =
   getCurrentInstance().appContext.config.globalProperties.$storage;
-
 const title =
   getCurrentInstance().appContext.config.globalProperties.$config?.Title;
 
-const menuRef = templateRef<ElRef | null>("menu", null);
-const route = useRoute();
-const router = useRouter();
-const routers = useRouter().options.routes;
-let usename = storageSession.getItem("info")?.username;
-const { locale, t } = useI18n();
+const {
+  logout,
+  backHome,
+  onPanel,
+  changeTitle,
+  handleResize,
+  menuSelect,
+  usename,
+  getDropdownItemStyle
+} = useNav();
 
-const getDropdownItemStyle = computed(() => {
-  return t => {
-    return {
-      background: locale.value === t ? "#1b2a47" : "",
-      color: locale.value === t ? "#f4f4f5" : "#000"
-    };
-  };
+onMounted(() => {
+  nextTick(() => {
+    handleResize(menuRef.value);
+  });
 });
 
 watch(
   () => locale.value,
   () => {
-    //@ts-ignore
-    // 动态title
-    document.title = t(unref(route.meta.title));
+    changeTitle(route.meta);
   }
 );
 
-// 退出登录
-const logout = (): void => {
-  storageSession.removeItem("info");
-  router.push("/login");
-};
-
-function onPanel() {
-  emitter.emit("openPanel");
-}
-
-const activeMenu = computed((): string => {
-  const { meta, path } = route;
-  if (meta.activeMenu) {
-    // @ts-ignore
-    return meta.activeMenu;
-  }
-  return path;
-});
-
-const menuSelect = (indexPath: string): void => {
-  let parentPath = "";
-  let parentPathIndex = indexPath.lastIndexOf("/");
-  if (parentPathIndex > 0) {
-    parentPath = indexPath.slice(0, parentPathIndex);
-  }
-  // 找到当前路由的信息
-  function findCurrentRoute(routes) {
-    return routes.map(item => {
-      if (item.path === indexPath) {
-        // 切换左侧菜单 通知标签页
-        emitter.emit("changLayoutRoute", {
-          indexPath,
-          parentPath
-        });
-      } else {
-        if (item.children) findCurrentRoute(item.children);
-      }
-    });
-  }
-  findCurrentRoute(routers);
-};
-
-function backHome() {
-  router.push("/welcome");
-}
-
-function handleResize() {
-  // @ts-ignore
-  menuRef.value.handleResize();
-}
-
-// 简体中文
 function translationCh() {
   instance.locale = { locale: "zh" };
   locale.value = "zh";
-  handleResize();
+  handleResize(menuRef.value);
 }
 
-// English
 function translationEn() {
   instance.locale = { locale: "en" };
   locale.value = "en";
-  handleResize();
+  handleResize(menuRef.value);
 }
-
-onMounted(() => {
-  nextTick(() => {
-    handleResize();
-  });
-});
 </script>
 
 <template>
@@ -135,12 +70,11 @@ onMounted(() => {
     </div>
     <el-menu
       ref="menu"
-      :default-active="activeMenu"
-      unique-opened
-      router
       class="horizontal-header-menu"
       mode="horizontal"
-      @select="menuSelect"
+      :default-active="route.path"
+      router
+      @select="indexPath => menuSelect(indexPath, routers)"
     >
       <sidebar-item
         v-for="route in usePermissionStoreHook().wholeMenus"
@@ -160,14 +94,14 @@ onMounted(() => {
         <template #dropdown>
           <el-dropdown-menu class="translation">
             <el-dropdown-item
-              :style="getDropdownItemStyle('zh')"
+              :style="getDropdownItemStyle(locale, 'zh')"
               @click="translationCh"
               ><el-icon class="check-zh" v-show="locale === 'zh'"
                 ><IconifyIconOffline icon="check" /></el-icon
               >简体中文</el-dropdown-item
             >
             <el-dropdown-item
-              :style="getDropdownItemStyle('en')"
+              :style="getDropdownItemStyle(locale, 'en')"
               @click="translationEn"
               ><el-icon class="check-en" v-show="locale === 'en'"
                 ><IconifyIconOffline icon="check" /></el-icon

+ 239 - 0
src/layout/components/sidebar/mixNav.vue

@@ -0,0 +1,239 @@
+<script setup lang="ts">
+import { useI18n } from "vue-i18n";
+import Notice from "../notice/index.vue";
+import { useNav } from "../../hooks/nav";
+import { templateRef } from "@vueuse/core";
+import avatars from "/@/assets/avatars.jpg";
+import { transformI18n } from "/@/plugins/i18n";
+import screenfull from "../screenfull/index.vue";
+import { useRoute, useRouter } from "vue-router";
+import { deviceDetection } from "/@/utils/deviceDetection";
+import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
+import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
+import { getParentPaths, findRouteByPath } from "/@/router/utils";
+import { usePermissionStoreHook } from "/@/store/modules/permission";
+import globalization from "/@/assets/svg/globalization.svg?component";
+import { ref, watch, nextTick, onMounted, getCurrentInstance } from "vue";
+
+const route = useRoute();
+const { locale } = useI18n();
+const routers = useRouter().options.routes;
+const menuRef = templateRef<ElRef | null>("menu", null);
+const instance =
+  getCurrentInstance().appContext.config.globalProperties.$storage;
+const wholeMenus = usePermissionStoreHook().wholeMenus;
+
+const {
+  logout,
+  onPanel,
+  changeTitle,
+  toggleSideBar,
+  handleResize,
+  menuSelect,
+  resolvePath,
+  pureApp,
+  usename,
+  getDropdownItemStyle
+} = useNav();
+
+let defaultActive = ref(null);
+
+function getDefaultActive(routePath) {
+  // 当前路由的父级路径
+  const parentRoutes = getParentPaths(routePath, wholeMenus)[0];
+  defaultActive.value = findRouteByPath(
+    parentRoutes,
+    wholeMenus
+  )?.children[0]?.path;
+}
+
+onMounted(() => {
+  getDefaultActive(route.path);
+  nextTick(() => {
+    handleResize(menuRef.value);
+  });
+});
+
+watch(
+  () => locale.value,
+  () => {
+    changeTitle(route.meta);
+  }
+);
+
+watch(
+  () => route.path,
+  () => {
+    getDefaultActive(route.path);
+  }
+);
+
+function translationCh() {
+  instance.locale = { locale: "zh" };
+  locale.value = "zh";
+  handleResize(menuRef.value);
+}
+
+function translationEn() {
+  instance.locale = { locale: "en" };
+  locale.value = "en";
+  handleResize(menuRef.value);
+}
+</script>
+
+<template>
+  <div class="horizontal-header">
+    <div
+      :class="classes.container"
+      :title="pureApp.sidebar.opened ? '点击折叠' : '点击展开'"
+      @click="toggleSideBar"
+    >
+      <svg
+        :fill="useEpThemeStoreHook().fill"
+        :class="[
+          'hamburger',
+          pureApp.sidebar.opened ? 'is-active-hamburger' : ''
+        ]"
+        viewBox="0 0 1024 1024"
+        xmlns="http://www.w3.org/2000/svg"
+        width="64"
+        height="64"
+      >
+        <path
+          d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
+        />
+      </svg>
+    </div>
+    <el-menu
+      ref="menu"
+      class="horizontal-header-menu"
+      mode="horizontal"
+      :default-active="defaultActive"
+      router
+      @select="indexPath => menuSelect(indexPath, routers)"
+    >
+      <el-menu-item
+        v-for="route in wholeMenus"
+        :key="route.path"
+        :index="resolvePath(route) || route.redirect"
+      >
+        <template #title>
+          <el-icon v-show="route.meta.icon" :class="route.meta.icon">
+            <component
+              :is="useRenderIcon(route.meta && route.meta.icon)"
+            ></component>
+          </el-icon>
+          <span>{{ transformI18n(route.meta.title, route.meta.i18n) }}</span>
+          <FontIcon
+            v-if="route.meta.extraIcon"
+            width="30px"
+            height="30px"
+            style="position: absolute; right: 10px"
+            :icon="route.meta.extraIcon.name"
+            :svg="route.meta.extraIcon.svg ? true : false"
+          ></FontIcon>
+        </template>
+      </el-menu-item>
+    </el-menu>
+    <div class="horizontal-header-right">
+      <!-- 通知 -->
+      <Notice id="header-notice" />
+      <!-- 全屏 -->
+      <screenfull id="header-screenfull" v-show="!deviceDetection()" />
+      <!-- 国际化 -->
+      <el-dropdown id="header-translation" trigger="click">
+        <globalization />
+        <template #dropdown>
+          <el-dropdown-menu class="translation">
+            <el-dropdown-item
+              :style="getDropdownItemStyle(locale, 'zh')"
+              @click="translationCh"
+              ><el-icon class="check-zh" v-show="locale === 'zh'"
+                ><IconifyIconOffline icon="check" /></el-icon
+              >简体中文</el-dropdown-item
+            >
+            <el-dropdown-item
+              :style="getDropdownItemStyle(locale, 'en')"
+              @click="translationEn"
+              ><el-icon class="check-en" v-show="locale === 'en'"
+                ><IconifyIconOffline icon="check" /></el-icon
+              >English</el-dropdown-item
+            >
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+      <!-- 退出登陆 -->
+      <el-dropdown trigger="click">
+        <span class="el-dropdown-link">
+          <img :src="avatars" />
+          <p>{{ usename }}</p>
+        </span>
+        <template #dropdown>
+          <el-dropdown-menu class="logout">
+            <el-dropdown-item @click="logout">
+              <IconifyIconOffline
+                icon="logout-circle-r-line"
+                style="margin: 5px"
+              />
+              {{ $t("buttons.hsLoginOut") }}</el-dropdown-item
+            >
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+      <el-icon
+        class="el-icon-setting"
+        :title="$t('buttons.hssystemSet')"
+        @click="onPanel"
+      >
+        <IconifyIconOffline icon="setting" />
+      </el-icon>
+    </div>
+  </div>
+</template>
+
+<style module="classes" scoped>
+.container {
+  padding: 0 15px;
+}
+</style>
+
+<style lang="scss" scoped>
+.hamburger {
+  width: 20px;
+  height: 20px;
+
+  &:hover {
+    cursor: pointer;
+  }
+}
+
+.is-active-hamburger {
+  transform: rotate(180deg);
+}
+
+.translation {
+  ::v-deep(.el-dropdown-menu__item) {
+    padding: 5px 40px;
+  }
+
+  .check-zh {
+    position: absolute;
+    left: 20px;
+  }
+
+  .check-en {
+    position: absolute;
+    left: 20px;
+  }
+}
+
+.logout {
+  max-width: 120px;
+
+  ::v-deep(.el-dropdown-menu__item) {
+    min-width: 100%;
+    display: inline-flex;
+    flex-wrap: wrap;
+  }
+}
+</style>

+ 18 - 14
src/layout/components/sidebar/sidebarItem.vue

@@ -1,21 +1,14 @@
 <script setup lang="ts">
-import {
-  ref,
-  PropType,
-  nextTick,
-  computed,
-  CSSProperties,
-  getCurrentInstance
-} from "vue";
+import { ref, PropType, nextTick, computed, CSSProperties } from "vue";
 import path from "path";
+import { useNav } from "../../hooks/nav";
 import { childrenType } from "../../types";
 import { transformI18n } from "/@/plugins/i18n";
 import { useAppStoreHook } from "/@/store/modules/app";
 import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
 
-const instance = getCurrentInstance().appContext.app.config.globalProperties;
-const menuMode = instance.$storage.layout?.layout === "vertical";
-const pureApp = useAppStoreHook();
+const { pureApp } = useNav();
+const menuMode = ["vertical", "mix"].includes(pureApp.layout);
 
 const props = defineProps({
   item: {
@@ -105,7 +98,6 @@ function hoverMenu(key) {
       : Object.assign(key, {
           showTooltip: false
         });
-
     hoverMenuMap.set(key, true);
   });
 }
@@ -132,8 +124,8 @@ function hasOneShowingChild(
 
 function resolvePath(routePath) {
   const httpReg = /^http(s?):\/\//;
-  if (httpReg.test(routePath)) {
-    return props.basePath + "/" + routePath;
+  if (httpReg.test(routePath) || httpReg.test(props.basePath)) {
+    return routePath || props.basePath;
   } else {
     return path.resolve(props.basePath, routePath);
   }
@@ -162,6 +154,18 @@ function resolvePath(routePath) {
           "
         ></component>
       </el-icon>
+      <div
+        v-if="
+          !pureApp.sidebar.opened &&
+          pureApp.layout === 'mix' &&
+          props.item?.pathList?.length === 2
+        "
+        :style="getDivStyle"
+      >
+        <span :style="getMenuTextStyle">
+          {{ transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n) }}
+        </span>
+      </div>
       <template #title>
         <div :style="getDivStyle">
           <span v-if="!menuMode">{{

+ 39 - 43
src/layout/components/sidebar/vertical.vue

@@ -1,60 +1,56 @@
 <script setup lang="ts">
 import Logo from "./logo.vue";
 import { emitter } from "/@/utils/mitt";
+import { useNav } from "../../hooks/nav";
 import SidebarItem from "./sidebarItem.vue";
 import { storageLocal } from "/@/utils/storage";
 import { useRoute, useRouter } from "vue-router";
-import { computed, ref, onBeforeMount } from "vue";
-import { useAppStoreHook } from "/@/store/modules/app";
+import { ref, computed, watch, onBeforeMount } from "vue";
+import { findRouteByPath, getParentPaths } from "/@/router/utils";
 import { usePermissionStoreHook } from "/@/store/modules/permission";
 
 const route = useRoute();
-const pureApp = useAppStoreHook();
-const router = useRouter().options.routes;
+const routers = useRouter().options.routes;
 const showLogo = ref(
   storageLocal.getItem("responsive-configure")?.showLogo ?? true
 );
-const isCollapse = computed(() => {
-  return !pureApp.getSidebarStatus;
-});
-const activeMenu = computed((): string => {
-  const { meta, path } = route;
-  if (meta.activeMenu) {
-    // @ts-ignore
-    return meta.activeMenu;
-  }
-  return path;
+
+const { pureApp, isCollapse, menuSelect } = useNav();
+
+let subMenuData = ref([]);
+
+const menuData = computed(() => {
+  return pureApp.layout === "mix"
+    ? subMenuData.value
+    : usePermissionStoreHook().wholeMenus;
 });
 
-const menuSelect = (indexPath: string): void => {
-  let parentPath = "";
-  let parentPathIndex = indexPath.lastIndexOf("/");
-  if (parentPathIndex > 0) {
-    parentPath = indexPath.slice(0, parentPathIndex);
-  }
-  // 找到当前路由的信息
-  // eslint-disable-next-line no-inner-declarations
-  function findCurrentRoute(routes) {
-    return routes.map(item => {
-      if (item.path === indexPath) {
-        // 切换左侧菜单 通知标签页
-        emitter.emit("changLayoutRoute", {
-          indexPath,
-          parentPath
-        });
-      } else {
-        if (item.children) findCurrentRoute(item.children);
-      }
-    });
-  }
-  findCurrentRoute(router);
-};
+function getSubMenuData(path) {
+  // path的上级路由组成的数组
+  const parentPathArr = getParentPaths(
+    path,
+    usePermissionStoreHook().wholeMenus
+  );
+  // 当前路由的父级路由信息
+  const parenetRoute = findRouteByPath(
+    parentPathArr[0] || path,
+    usePermissionStoreHook().wholeMenus
+  );
+  if (!parenetRoute?.children) return;
+  subMenuData.value = parenetRoute?.children;
+}
+getSubMenuData(route.path);
 
 onBeforeMount(() => {
   emitter.on("logoChange", key => {
     showLogo.value = key;
   });
 });
+
+watch(
+  () => route.path,
+  () => getSubMenuData(route.path)
+);
 </script>
 
 <template>
@@ -62,21 +58,21 @@ onBeforeMount(() => {
     <Logo v-if="showLogo" :collapse="isCollapse" />
     <el-scrollbar wrap-class="scrollbar-wrapper">
       <el-menu
-        :default-active="activeMenu"
+        :default-active="route.path"
         :collapse="isCollapse"
         unique-opened
         router
         :collapse-transition="false"
         mode="vertical"
         class="outer-most"
-        @select="menuSelect"
+        @select="indexPath => menuSelect(indexPath, routers)"
       >
         <sidebar-item
-          v-for="route in usePermissionStoreHook().wholeMenus"
-          :key="route.path"
-          :item="route"
+          v-for="routes in menuData"
+          :key="routes.path"
+          :item="routes"
           class="outer-most"
-          :base-path="route.path"
+          :base-path="routes.path"
         />
       </el-menu>
     </el-scrollbar>

+ 16 - 3
src/layout/components/tag/index.vue

@@ -106,7 +106,11 @@ const iconIsActive = computed(() => {
 
 const dynamicTagView = () => {
   const index = multiTags.value.findIndex(item => {
-    return item.path === route.path;
+    if (item?.query) {
+      return isEqual(route?.query, item?.query);
+    } else {
+      return item.path === route.path;
+    }
   });
   moveToView(index);
 };
@@ -423,6 +427,11 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
   });
 }
 
+function handleCommand(command: object) {
+  const { key, item } = command;
+  onClickDrop(key, item);
+}
+
 // 触发右键中菜单的点击事件
 function selectTag(key, item) {
   onClickDrop(key, item, currentSelect.value);
@@ -709,7 +718,11 @@ const getContextMenuStyle = computed((): CSSProperties => {
         </el-icon>
       </li>
       <li>
-        <el-dropdown trigger="click" placement="bottom-end">
+        <el-dropdown
+          trigger="click"
+          placement="bottom-end"
+          @command="handleCommand"
+        >
           <el-icon>
             <IconifyIconOffline icon="arrow-down" />
           </el-icon>
@@ -718,9 +731,9 @@ const getContextMenuStyle = computed((): CSSProperties => {
               <el-dropdown-item
                 v-for="(item, key) in tagsViews"
                 :key="key"
+                :command="{ key, item }"
                 :divided="item.divided"
                 :disabled="item.disabled"
-                @click="onClickDrop(key, item)"
               >
                 <component
                   :is="item.icon"

+ 110 - 0
src/layout/hooks/nav.ts

@@ -0,0 +1,110 @@
+import { computed } from "vue";
+import { router } from "/@/router";
+import { emitter } from "/@/utils/mitt";
+import { routeMetaType } from "../types";
+import { transformI18n } from "/@/plugins/i18n";
+import { storageSession } from "/@/utils/storage";
+import { useAppStoreHook } from "/@/store/modules/app";
+import { Title } from "../../../public/serverConfig.json";
+import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
+
+export function useNav() {
+  const pureApp = useAppStoreHook();
+  // 用户名
+  const usename: string = storageSession.getItem("info")?.username;
+
+  // 设置国际化选中后的样式
+  const getDropdownItemStyle = computed(() => {
+    return (locale, t) => {
+      return {
+        background: locale === t ? useEpThemeStoreHook().epThemeColor : "",
+        color: locale === t ? "#f4f4f5" : "#000"
+      };
+    };
+  });
+
+  const isCollapse = computed(() => {
+    return !pureApp.getSidebarStatus;
+  });
+
+  // 动态title
+  function changeTitle(meta: routeMetaType) {
+    if (Title)
+      document.title = `${transformI18n(meta.title, meta.i18n)} | ${Title}`;
+    else document.title = transformI18n(meta.title, meta.i18n);
+  }
+
+  // 退出登录
+  function logout() {
+    storageSession.removeItem("info");
+    router.push("/login");
+  }
+
+  function backHome() {
+    router.push("/welcome");
+  }
+
+  function onPanel() {
+    emitter.emit("openPanel");
+  }
+
+  function toggleSideBar() {
+    pureApp.toggleSideBar();
+  }
+
+  function handleResize(menuRef) {
+    menuRef.handleResize();
+  }
+
+  function resolvePath(route) {
+    const httpReg = /^http(s?):\/\//;
+    const routeChildPath = route.children[0]?.path;
+    if (httpReg.test(routeChildPath)) {
+      return route.path + "/" + routeChildPath;
+    } else {
+      return routeChildPath;
+    }
+  }
+
+  function menuSelect(indexPath: string, routers): void {
+    let parentPath = "";
+    const parentPathIndex = indexPath.lastIndexOf("/");
+    if (parentPathIndex > 0) {
+      parentPath = indexPath.slice(0, parentPathIndex);
+    }
+    // 找到当前路由的信息
+    function findCurrentRoute(indexPath: string, routes) {
+      return routes.map(item => {
+        if (item.path === indexPath) {
+          if (item.redirect) {
+            findCurrentRoute(item.redirect, item.children);
+          } else {
+            // 切换左侧菜单 通知标签页
+            emitter.emit("changLayoutRoute", {
+              indexPath,
+              parentPath
+            });
+          }
+        } else {
+          if (item.children) findCurrentRoute(indexPath, item.children);
+        }
+      });
+    }
+    findCurrentRoute(indexPath, routers);
+  }
+
+  return {
+    logout,
+    backHome,
+    onPanel,
+    changeTitle,
+    toggleSideBar,
+    menuSelect,
+    handleResize,
+    resolvePath,
+    isCollapse,
+    pureApp,
+    usename,
+    getDropdownItemStyle
+  };
+}

+ 6 - 2
src/layout/index.vue

@@ -171,7 +171,8 @@ const layoutHeader = defineComponent({
       },
       {
         default: () => [
-          !pureSetting.hiddenSideBar && layout.value.includes("vertical")
+          !pureSetting.hiddenSideBar &&
+          (layout.value.includes("vertical") || layout.value.includes("mix"))
             ? h(navbar)
             : h("div"),
           !pureSetting.hiddenSideBar && layout.value.includes("horizontal")
@@ -213,7 +214,10 @@ const layoutHeader = defineComponent({
       @click="useAppStoreHook().toggleSideBar()"
     />
     <Vertical
-      v-show="!pureSetting.hiddenSideBar && layout.includes('vertical')"
+      v-show="
+        !pureSetting.hiddenSideBar &&
+        (layout.includes('vertical') || layout.includes('mix'))
+      "
     />
     <div
       :class="[

+ 12 - 8
src/layout/types.ts

@@ -11,18 +11,20 @@ export const routerArrays: Array<RouteConfigs> = [
   }
 ];
 
+export type routeMetaType = {
+  title?: string;
+  i18n?: boolean;
+  icon?: string;
+  showLink?: boolean;
+  savedPosition?: boolean;
+  authority?: Array<string>;
+};
+
 export type RouteConfigs = {
   path?: string;
   parentPath?: string;
   query?: object;
-  meta?: {
-    title?: string;
-    i18n?: boolean;
-    icon?: string;
-    showLink?: boolean;
-    savedPosition?: boolean;
-    authority?: Array<string>;
-  };
+  meta?: routeMetaType;
   children?: RouteConfigs[];
   name?: string;
 };
@@ -71,6 +73,8 @@ export type childrenType = {
     };
   };
   showTooltip?: boolean;
+  parentId?: number;
+  pathList?: number[];
 };
 
 export type themeColorsType = {

+ 4 - 1
src/plugins/i18n/index.ts

@@ -11,7 +11,10 @@ import { storageLocal } from "/@/utils/storage";
  * @param isI18n  如果true,获取对应的消息,否则返回本身
  * @returns message
  */
-export function transformI18n(message: string | object = "", isI18n = false) {
+export function transformI18n(
+  message: string | unknown | object = "",
+  isI18n: boolean | unknown = false
+) {
   if (!message) {
     return "";
   }

+ 8 - 6
src/router/index.ts

@@ -6,6 +6,7 @@ import { split, findIndex } from "lodash-es";
 import { transformI18n } from "/@/plugins/i18n";
 import remainingRouter from "./modules/remaining";
 import { storageSession } from "/@/utils/storage";
+import { Title } from "../../public/serverConfig.json";
 import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
 import { usePermissionStoreHook } from "/@/store/modules/permission";
 import { Router, RouteMeta, createRouter, RouteRecordName } from "vue-router";
@@ -54,12 +55,13 @@ router.beforeEach((to: toRouteType, _from, next) => {
   const externalLink = to?.redirectedFrom?.fullPath;
   if (!externalLink)
     to.matched.some(item => {
-      item.meta.title
-        ? (document.title = transformI18n(
-            item.meta.title as string,
-            item.meta?.i18n as boolean
-          ))
-        : "";
+      if (!item.meta.title) return "";
+      if (Title)
+        document.title = `${transformI18n(
+          item.meta.title,
+          item.meta?.i18n
+        )} | ${Title}`;
+      else document.title = transformI18n(item.meta.title, item.meta?.i18n);
     });
   if (name) {
     if (_from?.name) {

+ 2 - 1
src/router/modules/index.ts

@@ -16,6 +16,7 @@ import {
   formatTwoStageRoutes,
   formatFlatteningRoutes
 } from "../utils";
+import { buildHierarchyTree } from "/@/utils/tree";
 
 // 原始静态路由(未做任何处理)
 const routes = [
@@ -32,7 +33,7 @@ const routes = [
 
 // 导出处理后的静态路由(三级及以上的路由全部拍成二级)
 export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
-  formatFlatteningRoutes(ascending(routes))
+  formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
 );
 
 // 用于渲染菜单,保持原始层级

+ 14 - 1
src/store/modules/epTheme.ts

@@ -8,16 +8,29 @@ export const useEpThemeStore = defineStore({
   state: () => ({
     epThemeColor:
       storageLocal.getItem("responsive-layout")?.epThemeColor ??
-      getConfig().EpThemeColor
+      getConfig().EpThemeColor,
+    epTheme:
+      storageLocal.getItem("responsive-layout")?.theme ?? getConfig().Theme
   }),
   getters: {
     getEpThemeColor() {
       return this.epThemeColor;
+    },
+    // 用于mix导航模式下hamburger-svg的fill属性
+    fill() {
+      if (this.epTheme === "light") {
+        return "#409eff";
+      } else if (this.epTheme === "yellow") {
+        return "#d25f00";
+      } else {
+        return "#fff";
+      }
     }
   },
   actions: {
     setEpThemeColor(newColor) {
       const layout = storageLocal.getItem("responsive-layout");
+      this.epTheme = layout?.theme;
       this.epThemeColor = newColor;
       layout.epThemeColor = newColor;
       storageLocal.setItem("responsive-layout", layout);

+ 61 - 0
src/style/sidebar.scss

@@ -638,3 +638,64 @@ body[layout="horizontal"] {
     transition: none !important;
   }
 }
+
+body[layout="mix"] {
+  $sideBarWidth: 210px;
+  @include merge-style($sideBarWidth);
+
+  .el-menu {
+    --el-menu-hover-bg-color: transparent !important;
+  }
+
+  .hideSidebar {
+    .fixed-header {
+      width: calc(100% - 54px);
+      transition: width 0.28s;
+    }
+
+    .sidebar-container {
+      width: 54px !important;
+    }
+
+    .main-container {
+      margin-left: 54px;
+    }
+
+    .submenu-title-noDropdown {
+      padding: 0 !important;
+      position: relative;
+
+      .el-tooltip {
+        padding: 0 !important;
+      }
+    }
+
+    /* 菜单折叠 */
+    .el-menu--collapse {
+      .el-sub-menu {
+        & > .el-sub-menu__title {
+          & > span {
+            height: 0;
+            width: 0;
+            overflow: hidden;
+            visibility: hidden;
+            display: inline-block;
+          }
+        }
+      }
+
+      /* 无子菜单 */
+      .el-menu-item [class^="el-icon"] {
+        right: 5px;
+      }
+
+      .el-sub-menu__title [class^="el-icon"] {
+        right: 2px;
+      }
+
+      .submenu-title-noDropdown {
+        background: transparent !important;
+      }
+    }
+  }
+}

+ 19 - 0
src/utils/tree.ts

@@ -46,3 +46,22 @@ export function deleteChildren(menuTree, pathList = []) {
   }
   return menuTree;
 }
+
+// 创建层级关系
+export function buildHierarchyTree(menuTree, pathList = []) {
+  if (!Array.isArray(menuTree)) {
+    console.warn("menuTree must be an array");
+    return;
+  }
+  if (!menuTree || menuTree.length === 0) return;
+  for (const [key, node] of menuTree.entries()) {
+    node.id = key;
+    node.parentId = pathList.length ? pathList[pathList.length - 1] : null;
+    node.pathList = [...pathList, node.id];
+    const hasChildren = node.children && node.children.length > 0;
+    if (hasChildren) {
+      buildHierarchyTree(node.children, node.pathList);
+    }
+  }
+  return menuTree;
+}

+ 1 - 0
src/views/components/draggable/index.vue

@@ -140,6 +140,7 @@ onMounted(() => {
 .item-cut {
   font-size: 1.5em;
   height: 77px;
+  line-height: 77px;
   text-align: center;
   border: 1px solid #e5e4e9;
   cursor: move;

Някои файлове не бяха показани, защото твърде много файлове са промени