Explorar el Código

feat: 菜单组件

runningwater hace 5 meses
padre
commit
a30f8ba613

+ 8 - 0
components.d.ts

@@ -10,15 +10,23 @@ declare module 'vue' {
   export interface GlobalComponents {
     Dashboard: typeof import('./src/views/Dashboard/index.vue')['default']
     ElButton: typeof import('element-plus/es')['ElButton']
+    ElCard: typeof import('element-plus/es')['ElCard']
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
+    ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
+    Guide: typeof import('./src/views/guide/index.vue')['default']
     Hamburger: typeof import('./src/components/Hamburger/index.vue')['default']
+    Menu: typeof import('./src/views/system/menu/index.vue')['default']
     Navbar: typeof import('./src/layout/components/Navbar.vue')['default']
+    Role: typeof import('./src/views/system/role/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     Sidebar: typeof import('./src/layout/components/Sidebar/index.vue')['default']
+    SidebarItem: typeof import('./src/layout/components/Sidebar/SidebarItem.vue')['default']
+    SidebarItemLink: typeof import('./src/layout/components/Sidebar/SidebarItemLink.vue')['default']
     SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
     Test: typeof import('./src/views/Dashboard/test.vue')['default']
+    User: typeof import('./src/views/system/user/index.vue')['default']
   }
 }

+ 3 - 1
package.json

@@ -14,9 +14,11 @@
   },
   "dependencies": {
     "@iconify/vue": "^5.0.0",
+    "@types/path-browserify": "^1.0.3",
     "@unocss/transformer-directives": "66.1.0-beta.7",
     "element-plus": "^2.9.7",
     "normalize.css": "^8.0.1",
+    "path-browserify": "^1.0.1",
     "pinia": "^3.0.1",
     "pinia-plugin-persistedstate": "^4.3.0",
     "unplugin-element-plus": "^0.9.1",
@@ -48,7 +50,7 @@
     "unocss": "66.1.0-beta.7",
     "unplugin-auto-import": "^19.1.2",
     "unplugin-vue-components": "^28.4.1",
-    "vite": "^6.2.0",
+    "vite": "^6.3.4",
     "vue-tsc": "^2.2.4"
   },
   "lint-staged": {

+ 36 - 18
pnpm-lock.yaml

@@ -1,9 +1,16 @@
 lockfileVersion: '6.0'
 
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
 dependencies:
   '@iconify/vue':
     specifier: ^5.0.0
     version: 5.0.0(vue@3.5.13)
+  '@types/path-browserify':
+    specifier: ^1.0.3
+    version: 1.0.3
   '@unocss/transformer-directives':
     specifier: 66.1.0-beta.7
     version: 66.1.0-beta.7
@@ -13,6 +20,9 @@ dependencies:
   normalize.css:
     specifier: ^8.0.1
     version: 8.0.1
+  path-browserify:
+    specifier: ^1.0.1
+    version: 1.0.1
   pinia:
     specifier: ^3.0.1
     version: 3.0.1(typescript@5.7.2)(vue@3.5.13)
@@ -56,7 +66,7 @@ devDependencies:
     version: 66.1.3
   '@vitejs/plugin-vue':
     specifier: ^5.2.1
-    version: 5.2.1(vite@6.2.0)(vue@3.5.13)
+    version: 5.2.1(vite@6.3.4)(vue@3.5.13)
   '@vue/tsconfig':
     specifier: ^0.7.0
     version: 0.7.0(typescript@5.7.2)(vue@3.5.13)
@@ -95,7 +105,7 @@ devDependencies:
     version: 8.28.0(eslint@9.23.0)(typescript@5.7.2)
   unocss:
     specifier: 66.1.0-beta.7
-    version: 66.1.0-beta.7(postcss@8.5.4)(vite@6.2.0)(vue@3.5.13)
+    version: 66.1.0-beta.7(postcss@8.5.4)(vite@6.3.4)(vue@3.5.13)
   unplugin-auto-import:
     specifier: ^19.1.2
     version: 19.1.2
@@ -103,8 +113,8 @@ devDependencies:
     specifier: ^28.4.1
     version: 28.4.1(vue@3.5.13)
   vite:
-    specifier: ^6.2.0
-    version: 6.2.0(@types/node@22.15.29)(sass@1.86.0)
+    specifier: ^6.3.4
+    version: 6.3.4(@types/node@22.15.29)(sass@1.86.0)
   vue-tsc:
     specifier: ^2.2.4
     version: 2.2.4(typescript@5.7.2)
@@ -1168,6 +1178,10 @@ packages:
       undici-types: 6.21.0
     dev: true
 
+  /@types/path-browserify@1.0.3:
+    resolution: {integrity: sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw==}
+    dev: false
+
   /@types/web-bluetooth@0.0.16:
     resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
     dev: false
@@ -1288,7 +1302,7 @@ packages:
       eslint-visitor-keys: 4.2.0
     dev: true
 
-  /@unocss/astro@66.1.0-beta.7(vite@6.2.0)(vue@3.5.13):
+  /@unocss/astro@66.1.0-beta.7(vite@6.3.4)(vue@3.5.13):
     resolution: {integrity: sha512-cqimcWi/JNwNIMFHi3MCWUlF64y867AQmXd1/L3ZpGwb45EdYY2T7RsTsFwh4POdDQT1GRKwpAeYObOs8DhExQ==}
     peerDependencies:
       vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0
@@ -1298,8 +1312,8 @@ packages:
     dependencies:
       '@unocss/core': 66.1.0-beta.7
       '@unocss/reset': 66.1.0-beta.7
-      '@unocss/vite': 66.1.0-beta.7(vite@6.2.0)(vue@3.5.13)
-      vite: 6.2.0(@types/node@22.15.29)(sass@1.86.0)
+      '@unocss/vite': 66.1.0-beta.7(vite@6.3.4)(vue@3.5.13)
+      vite: 6.3.4(@types/node@22.15.29)(sass@1.86.0)
     transitivePeerDependencies:
       - vue
     dev: true
@@ -1522,7 +1536,7 @@ packages:
       '@unocss/core': 66.1.0-beta.7
     dev: true
 
-  /@unocss/vite@66.1.0-beta.7(vite@6.2.0)(vue@3.5.13):
+  /@unocss/vite@66.1.0-beta.7(vite@6.3.4)(vue@3.5.13):
     resolution: {integrity: sha512-7dDdFdaO6Mz7xTd/jZYqe8NkBn4CjJFN7uPK9xLLZEPNFT6anOkTv2UB9qU5lb4NcHFr5dyCVrRtqb0X4rmOMQ==}
     peerDependencies:
       vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0
@@ -1535,19 +1549,19 @@ packages:
       magic-string: 0.30.17
       tinyglobby: 0.2.14
       unplugin-utils: 0.2.4
-      vite: 6.2.0(@types/node@22.15.29)(sass@1.86.0)
+      vite: 6.3.4(@types/node@22.15.29)(sass@1.86.0)
     transitivePeerDependencies:
       - vue
     dev: true
 
-  /@vitejs/plugin-vue@5.2.1(vite@6.2.0)(vue@3.5.13):
+  /@vitejs/plugin-vue@5.2.1(vite@6.3.4)(vue@3.5.13):
     resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==}
     engines: {node: ^18.0.0 || >=20.0.0}
     peerDependencies:
       vite: ^5.0.0 || ^6.0.0
       vue: ^3.2.25
     dependencies:
-      vite: 6.2.0(@types/node@22.15.29)(sass@1.86.0)
+      vite: 6.3.4(@types/node@22.15.29)(sass@1.86.0)
       vue: 3.5.13(typescript@5.7.2)
     dev: true
 
@@ -2145,6 +2159,7 @@ packages:
     resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
     engines: {node: '>=0.10'}
     hasBin: true
+    requiresBuild: true
     dev: true
     optional: true
 
@@ -3062,6 +3077,7 @@ packages:
 
   /node-addon-api@7.1.1:
     resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+    requiresBuild: true
     dev: true
     optional: true
 
@@ -3195,7 +3211,6 @@ packages:
 
   /path-browserify@1.0.1:
     resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
-    dev: true
 
   /path-exists@4.0.0:
     resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
@@ -3725,7 +3740,7 @@ packages:
       unplugin-utils: 0.2.4
     dev: false
 
-  /unocss@66.1.0-beta.7(postcss@8.5.4)(vite@6.2.0)(vue@3.5.13):
+  /unocss@66.1.0-beta.7(postcss@8.5.4)(vite@6.3.4)(vue@3.5.13):
     resolution: {integrity: sha512-LFS45xWUOfu1+4EaFlSvpcXEJ6ZYwZ3HMmQpgKRvMmp6WAcv+WQEgvgM6Y/ar8TIFBpXwr5fvSM/OEXesqX7Ng==}
     engines: {node: '>=14'}
     peerDependencies:
@@ -3737,7 +3752,7 @@ packages:
       vite:
         optional: true
     dependencies:
-      '@unocss/astro': 66.1.0-beta.7(vite@6.2.0)(vue@3.5.13)
+      '@unocss/astro': 66.1.0-beta.7(vite@6.3.4)(vue@3.5.13)
       '@unocss/cli': 66.1.0-beta.7
       '@unocss/core': 66.1.0-beta.7
       '@unocss/postcss': 66.1.0-beta.7(postcss@8.5.4)
@@ -3755,8 +3770,8 @@ packages:
       '@unocss/transformer-compile-class': 66.1.0-beta.7
       '@unocss/transformer-directives': 66.1.0-beta.7
       '@unocss/transformer-variant-group': 66.1.0-beta.7
-      '@unocss/vite': 66.1.0-beta.7(vite@6.2.0)(vue@3.5.13)
-      vite: 6.2.0(@types/node@22.15.29)(sass@1.86.0)
+      '@unocss/vite': 66.1.0-beta.7(vite@6.3.4)(vue@3.5.13)
+      vite: 6.3.4(@types/node@22.15.29)(sass@1.86.0)
     transitivePeerDependencies:
       - postcss
       - supports-color
@@ -3855,8 +3870,8 @@ packages:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
     dev: true
 
-  /vite@6.2.0(@types/node@22.15.29)(sass@1.86.0):
-    resolution: {integrity: sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==}
+  /vite@6.3.4(@types/node@22.15.29)(sass@1.86.0):
+    resolution: {integrity: sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==}
     engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
     hasBin: true
     peerDependencies:
@@ -3897,9 +3912,12 @@ packages:
     dependencies:
       '@types/node': 22.15.29
       esbuild: 0.25.5
+      fdir: 6.4.5(picomatch@4.0.2)
+      picomatch: 4.0.2
       postcss: 8.5.4
       rollup: 4.41.1
       sass: 1.86.0
+      tinyglobby: 0.2.14
     optionalDependencies:
       fsevents: 2.3.3
     dev: true

+ 59 - 0
src/layout/components/Sidebar/SidebarItem.vue

@@ -0,0 +1,59 @@
+<template>
+  <template v-if="!item.meta?.hidden">
+    <sidebar-item-link
+      v-if="filteredChildren.length <= 1 && !item.meta?.alwaysShow"
+      :to="resolvePath(singleChildRoute.path)"
+    >
+      <el-menu-item :index="resolvePath(singleChildRoute.path)">
+        <el-icon><svg-icon :icon-name="iconName" /></el-icon>
+        <template #title>{{ singleChildRoute.meta?.title }}</template>
+      </el-menu-item>
+    </sidebar-item-link>
+    <!--  else  -->
+    <el-sub-menu v-else :index="item.path">
+      <template #title>
+        <el-icon><svg-icon :icon-name="iconName" /></el-icon>
+        <span>{{ item.meta?.title }}</span>
+      </template>
+      <sidebar-item
+        v-for="child of filteredChildren"
+        :key="child.path"
+        :item="child"
+        :base-path="resolvePath(child.path)"
+      ></sidebar-item>
+    </el-sub-menu>
+  </template>
+</template>
+<script lang="ts" setup>
+import type { RouteRecordRaw } from "vue-router";
+import path from "path-browserify";
+
+const { item, basePath } = defineProps<{
+  item: RouteRecordRaw;
+  basePath: string;
+}>();
+
+// 如果只有一个儿子,直接渲染
+// 如果菜单对应的 children 有多个,使用 el-submenu 去渲染
+const filteredChildren = computed(() => {
+  return (item.children || []).filter((child) => !child.meta?.hidden);
+});
+// 要渲染的路由 system => children[]
+const singleChildRoute = computed(
+  () =>
+    filteredChildren.value?.length === 1
+      ? filteredChildren.value[0]
+      : { ...item, path: "" }
+  // 此处我们将自己的 pth 设置为 "" 防止重复拼接
+);
+// 要渲染的图标
+const iconName = computed<string>(
+  () => singleChildRoute.value.meta?.icon as string
+);
+
+// 解析路径
+// /system  /system/menu -> /system/menu
+// /  dashboard  -> /dashboard
+const resolvePath = (childPath: string) => path.join(basePath, childPath);
+</script>
+<style lang="scss" scoped></style>

+ 21 - 0
src/layout/components/Sidebar/SidebarItemLink.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import { isExternal } from "@/utils/validate.ts";
+
+const { to } = defineProps<{
+  to: string;
+}>();
+
+const isExt = computed(() => isExternal(to));
+const componentType = computed(() => {
+  return isExt.value ? "a" : "router-link";
+});
+const componentProps = computed(() => {
+  return isExt.value ? { href: to, target: "_blank" } : { to };
+});
+</script>
+
+<template>
+  <component :is="componentType" v-bind="componentProps">
+    <slot></slot>
+  </component>
+</template>

+ 8 - 4
src/layout/components/Sidebar/index.vue

@@ -8,15 +8,19 @@
     :active-text-color="variables.menuActiveText"
     :collapse="sidebar.opened"
   >
-    <el-menu-item index="/dashboard">
-      <el-icon><settring /></el-icon>
-      <template #title>Navigator</template>
-    </el-menu-item>
+    <sidebar-item
+      v-for="rou in routes"
+      :key="rou.path"
+      :item="rou"
+      :base-path="rou.path"
+    ></sidebar-item>
   </el-menu>
 </template>
 <script lang="ts" setup>
 import variables from "@/style/variables.module.scss";
 import { useAppStore } from "@/stores/app";
+import { routes } from "@/router";
+
 const route = useRoute();
 const defaultActive = computed(() => {
   return route.path;

+ 83 - 2
src/router/index.ts

@@ -5,7 +5,7 @@ import {
 } from "vue-router";
 import Layout from "@/layout/index.vue";
 
-const routes: RouteRecordRaw[] = [
+export const constantRoutes: RouteRecordRaw[] = [
   {
     path: "/",
     component: Layout,
@@ -14,12 +14,93 @@ const routes: RouteRecordRaw[] = [
       {
         path: "dashboard",
         name: "Dashboard",
-        component: () => import("@/views/Dashboard/index.vue")
+        component: () => import("@/views/Dashboard/index.vue"),
+        meta: {
+          title: "dashboard",
+          icon: "ant-design:bank-outlined",
+          affix: true,
+          noCache: true
+        }
       }
     ]
   }
 ];
 
+export const asyncRoutes: RouteRecordRaw[] = [
+  {
+    path: "/guide",
+    component: Layout,
+    redirect: "/guide/index",
+    children: [
+      {
+        path: "index",
+        component: () => import("@/views/guide/index.vue"),
+        name: "Guide",
+        meta: {
+          title: "guide",
+          icon: "ant-design:car-twotone",
+          noCache: true
+        }
+      }
+    ]
+  },
+  {
+    path: "/system",
+    component: Layout,
+    redirect: "/system/menu",
+    meta: {
+      title: "system",
+      icon: "ant-design:unlock-filled",
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: "menu",
+        name: "menu",
+        component: () => import("@/views/system/menu/index.vue"),
+        meta: {
+          title: "menu",
+          icon: "ant-design:unlock-filled"
+        }
+      },
+      {
+        path: "role",
+        name: "role",
+        component: () => import("@/views/system/role/index.vue"),
+        meta: {
+          title: "role",
+          icon: "ant-design:unlock-filled"
+        }
+      },
+      {
+        path: "user",
+        name: "user",
+        component: () => import("@/views/system/user/index.vue"),
+        meta: {
+          title: "user",
+          icon: "ant-design:unlock-filled"
+        }
+      }
+    ]
+  },
+  {
+    path: "/external-link",
+    component: Layout,
+    children: [
+      {
+        path: "https://www.baidu.com",
+        redirect: "/",
+        meta: {
+          title: "External Link Baidu",
+          icon: "ant-design:link-outlined"
+        }
+      }
+    ]
+  }
+];
+
+export const routes: RouteRecordRaw[] = [...constantRoutes, ...asyncRoutes];
+
 export default createRouter({
   history: createWebHistory(), // createWebHashHistory() for hash mode
   routes // short for `routes: routes`

+ 14 - 0
src/router/typings.d.ts

@@ -0,0 +1,14 @@
+import "vue-router";
+
+// 给模块添加额外类型, ts 中的接口合并
+declare module "vue-router" {
+  interface RouteMeta {
+    icon?: string; // 菜单图标
+    title?: string; // 菜单名称
+    hidden?: boolean; // 是否在菜单中隐藏
+    alwaysShow?: boolean; // 是否总是显示根路由
+    breadcrumb?: boolean; // 是否显示面包屑
+    affix?: boolean; // 是否固定在 tags-view 中
+    noCache?: boolean; // 是否不缓存
+  }
+}

+ 4 - 0
src/style/index.scss

@@ -4,4 +4,8 @@
   --sidebar-width: #{variables.$sideBarWidth};
   --navbar-height: #{variables.$navBarHeight};
   --tagsview-height: #{variables.$tagsViewHeight};
+  --menu-bg: #{variables.$menuBg};
+}
+a {
+  @apply decoration-none active:(decoration-none) hover:(decoration-none);
 }

+ 9 - 0
src/views/guide/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div>
+    <el-card>
+      <template #header>
+        <span>引导页</span>
+      </template>
+    </el-card>
+  </div>
+</template>

+ 9 - 0
src/views/system/menu/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div>
+    <el-card>
+      <template #header>
+        <span>菜单管理</span>
+      </template>
+    </el-card>
+  </div>
+</template>

+ 9 - 0
src/views/system/role/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div>
+    <el-card>
+      <template #header>
+        <span>角色管理</span>
+      </template>
+    </el-card>
+  </div>
+</template>

+ 9 - 0
src/views/system/user/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div>
+    <el-card>
+      <template #header>
+        <span>用户管理</span>
+      </template>
+    </el-card>
+  </div>
+</template>