vue3+element-plus+vite實(shí)現(xiàn)動(dòng)態(tài)路由菜單方式
1. 環(huán)境搭建
1.1 新建一個(gè)vite搭建的vue3項(xiàng)目
先執(zhí)行以下命令
npm create vite@latest my-project(你的項(xiàng)目名)
1.2 選擇項(xiàng)目框架 vue
1.3 選擇語言類型 ts
1.4 執(zhí)行命令進(jìn)入到新建的項(xiàng)目文件中
cd my-project
1.5 下載依賴
npm i
下載項(xiàng)目中需要使用到的環(huán)境
npm install vue-router@4 pinia element-plus @element-plus/icons-vue
1.6 完善項(xiàng)目目錄結(jié)構(gòu)以及環(huán)境配置
1.6.1 先清空App.vue文件中內(nèi)容,增加router-view作為路由出口
<template> <router-view /> </template> <script setup lang="ts"> </script> <style scoped lang="scss"> #app { width: 100vw; height: 100vh; display: flex; justify-content: space-around; } </style>
1.6.2 在src目錄下新建文件夾layout,在該文件中新建文件AppLayout.vue (文件名看自己)
1.6.3 在src目錄下分別新建文件夾store和router分別用來pinia狀態(tài)管理和路由管理
1.6.3.1 router文件夾中新建兩個(gè)文件一個(gè)index.ts用來初始化路由和存放靜態(tài)路由一個(gè)dynamicRoutes.ts存放處理動(dòng)態(tài)路由
// router/dynamicRoutes.ts // 更新 initDynamicRoutes,確保 dynamicRoutes 被更新 import router from './index'; import { useRouteStore } from '@/store/index'; // 導(dǎo)入 store import type { RouteRecordRaw, RouteRecordRedirectOption } from 'vue-router'; // 定義菜單項(xiàng)類型,確保 `name` 是 `string` type MenuItem = Omit<RouteRecordRaw, 'component' | 'children' | 'redirect'> & { name: string; // 必須有 name 屬性 path: string; // 必須有 path 屬性 component?: () => Promise<Component>; // 用于動(dòng)態(tài)加載組件的路徑 children?: MenuItem[]; // 子路由類型 redirect?: string; // 調(diào)整 redirect 為更簡單的 string 類型 meta?: { title: string; }; }; // Vite 支持使用特殊的 import.meta.glob 函數(shù)從文件系統(tǒng)導(dǎo)入多個(gè)模塊 const modules: Record<string, () => Promise<Component>> = import.meta.glob('../views/**/**.vue'); // 初始化動(dòng)態(tài)路由 export const initDynamicRoutes = (menuData: MenuItem[]) => { const routeStore = useRouteStore(); // 獲取 store const routerList: MenuItem[] = []; const addedRoutes = new Set(); // 用于跟蹤已添加的路由,防止重復(fù)添加 // 遞歸處理路由 const processRoutes = (routes: MenuItem[]): MenuItem[] => { return routes.map((item) => { if (addedRoutes.has(item.name)) return null; // 防止重復(fù)處理 addedRoutes.add(item.name); // 標(biāo)記路由為已處理 const componentLoader = modules[`../views${item.component}.vue`]; const route: MenuItem = { path: item.path, name: item.name as string, component: componentLoader , // 提供默認(rèn)組件以防找不到 meta: item.meta, }; // 如果有子路由,遞歸處理 if (item.children && item.children.length > 0) { route.children = processRoutes(item.children); route.redirect = route.children[0]?.path; // 默認(rèn)重定向到第一個(gè)子路由 } else { route.children = undefined; // 明確設(shè)置為 undefined } return route; }).filter((route) => route !== null) as MenuItem[]; // 過濾掉 null 項(xiàng) }; // 頂級(jí)路由處理 const parentRouter = processRoutes(menuData); // 根路由配置 routerList.push({ path: '/', name: 'home', component: () => import('../layout/AppLayout.vue'), children: parentRouter, // 頂級(jí)路由作為子路由 redirect: parentRouter[0]?.path || '/', // 確保有默認(rèn)重定向路徑 }); // 將路由存儲(chǔ)到 store 中 routeStore.dynamicRoutes = routerList; // 添加路由到 Vue Router routerList.forEach((route) => { router.addRoute(route as RouteRecordRaw); }); };
// router/index.ts import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; import { useRouteStore } from "@/store"; // 靜態(tài)路由 const routes: RouteRecordRaw[] = [ { path: "/login", name: "login", component: () => import("@/views/login/index.vue"), }, { path: "/404", component: () => import("@/views/error-page/404.vue"), }, { path: "/401", component: () => import("@/views/error-page/401.vue"), }, // 匹配所有路徑 { path: "/:pathMatch(.*)", redirect: "/login" }, ]; // 創(chuàng)建路由 const router = createRouter({ history: createWebHashHistory(), // 路由模式 routes, // 靜態(tài)路由 }); // 路由守衛(wèi):初始化時(shí)跳轉(zhuǎn)到上次訪問的頁面 window.addEventListener('DOMContentLoaded', () => { const routeStore = useRouteStore() const beforeReloadRoute = sessionStorage.getItem('beforeReloadRoute') if (beforeReloadRoute) { const to = JSON.parse(beforeReloadRoute) routeStore.beforeRouter = to.path // 清除保存的路由信息 sessionStorage.removeItem('beforeReloadRoute') // 導(dǎo)航回刷新前的路由 router.replace(to) const keys = Object.keys(to) if (keys.includes('name')) { sessionStorage.setItem('roterPath', JSON.stringify(to.name)) } } }) // 在頁面即將刷新時(shí)保存當(dāng)前路由信息 window.addEventListener('beforeunload', () => { const currentRoute = JSON.stringify(router.currentRoute.value) sessionStorage.setItem('beforeReloadRoute', currentRoute) }) export default router;
1.6.3.2 實(shí)現(xiàn)路由持久化和白名單,需要在src目錄下新建一個(gè)permission.ts文件
import { createVNode, render } from 'vue'; import { initDynamicRoutes } from '@/router/dynamicRoutes'; import router from './router/index'; import loadingBar from '@/component/loadingBar.vue'; import Cookies from 'js-cookie'; // 引入 js-cookie import { useRouteStore } from '@/store/index'; import menuData from '/public/dynamicRouter.json'; // 導(dǎo)入動(dòng)態(tài)菜單數(shù)據(jù) const whileList = ['/login']; // 白名單 const Vnode = createVNode(loadingBar); render(Vnode, document.body); router.beforeEach(async (to, from, next) => { const routeStore = useRouteStore(); // 獲取 Pinia 中的路由狀態(tài) const token = Cookies.get('token'); // 從 cookie 獲取 token // 判斷是否有 token,存在則說明用戶已登錄 if (token) { // 檢查是否已經(jīng)加載過動(dòng)態(tài)路由 if (routeStore.dynamicRoutes.length === 0) { // 檢查是否有持久化的動(dòng)態(tài)路由 const persistedRoutes = sessionStorage.getItem('dynamicRoutes'); // 使用 sessionStorage if (persistedRoutes) { // 如果有持久化的動(dòng)態(tài)路由,直接從 sessionStorage 加載 const routerList = JSON.parse(persistedRoutes); initDynamicRoutes(routerList); // 動(dòng)態(tài)初始化路由 routeStore.setDynamicRoutes(routerList); // 將動(dòng)態(tài)路由存入 Pinia next({ ...to, replace: true }); // 確保動(dòng)態(tài)路由加載后再跳轉(zhuǎn) Vnode.component?.exposed?.startLoading(); // 啟動(dòng)加載條 } else { // 如果沒有持久化的動(dòng)態(tài)路由,則使用靜態(tài)的 dynamicRouter.json const dynamicRoutes = initDynamicRoutes(menuData); // 動(dòng)態(tài)初始化路由 if (dynamicRoutes !== undefined) { routeStore.setDynamicRoutes(dynamicRoutes); // 將動(dòng)態(tài)路由存入 Pinia sessionStorage.setItem('dynamicRoutes', JSON.stringify(dynamicRoutes)); // 存儲(chǔ)動(dòng)態(tài)路由到 sessionStorage next({ ...to, replace: true }); // 確保動(dòng)態(tài)路由加載后再跳轉(zhuǎn) Vnode.component?.exposed?.startLoading(); // 啟動(dòng)加載條 } else { next('/login'); // 如果沒有動(dòng)態(tài)路由信息,跳轉(zhuǎn)到登錄頁面 } } } else { next(); // 如果已經(jīng)加載過動(dòng)態(tài)路由,直接跳轉(zhuǎn) } } else { // 如果沒有 token,判斷是否在白名單中 if (whileList.includes(to.path)) { next(); // 白名單路由放行 } else { next('/login'); // 否則跳轉(zhuǎn)到登錄頁 } } }); router.afterEach(() => { Vnode.component?.exposed?.endLoading(); // 結(jié)束加載條 });
1.6.3.2 store文件夾下新建文件index.ts初始化pinia倉
// store/index.ts import { createPinia } from 'pinia'; import { useRouteStore } from './useRouteStore'; import { useUserStore } from './tokenStore'; // 創(chuàng)建 pinia 實(shí)例 const pinia = createPinia(); // 將所有 store 模塊暴露 export { pinia, useRouteStore, useUserStore };
1.6.3.2 store文件夾下新建文件useRouteStore.ts處理存儲(chǔ)動(dòng)態(tài)路由文件
import { defineStore } from 'pinia'; import { ref } from 'vue'; import { initDynamicRoutes } from "@/router/dynamicRoutes"; // 導(dǎo)入初始化動(dòng)態(tài)路由的方法 import type { RouteRecordRaw, RouteRecordRedirectOption } from 'vue-router'; // 定義菜單項(xiàng)類型,確保 `name` 是 `string` type MenuItem = Omit<RouteRecordRaw, 'component' | 'children' | 'redirect'> & { name: string; // 必須有 name 屬性 path: string; // 必須有 path 屬性 component?: () => Promise<Component>; // 用于動(dòng)態(tài)加載組件的路徑 children?: MenuItem[]; // 子路由類型 redirect?: string; // 調(diào)整 redirect 為更簡單的 string 類型 meta?: { title: string; }; }; // 定義路由數(shù)據(jù) Store export const useRouteStore = defineStore('route', () => { // 存儲(chǔ)菜單數(shù)據(jù) const menuData = ref<MenuItem[]>([]); // 根據(jù)你的菜單數(shù)據(jù)結(jié)構(gòu)調(diào)整類型 // 存儲(chǔ)動(dòng)態(tài)路由數(shù)據(jù) const dynamicRoutes = ref<MenuItem[]>([]); // 存儲(chǔ)是否已初始化路由的狀態(tài) const isRoutesInitialized = ref<boolean>(false); // 存儲(chǔ)上一次頁面刷新的路由 const beforeRouter = ref<string>(''); // 初始化動(dòng)態(tài)路由 const setDynamicRoutes = (menu: any[]) => { // 只在未初始化路由時(shí)執(zhí)行 if (!isRoutesInitialized.value) { // 調(diào)用 initDynamicRoutes 函數(shù)來生成動(dòng)態(tài)路由 initDynamicRoutes(menu); // 將菜單數(shù)據(jù)存儲(chǔ)到狀態(tài)中 menuData.value = menu; // 設(shè)置已初始化狀態(tài) isRoutesInitialized.value = true; } }; // 獲取動(dòng)態(tài)路由 const getDynamicRoutes = () => { return dynamicRoutes.value; }; // 更新動(dòng)態(tài)路由 const setUpdatedDynamicRoutes = (routes: MenuItem[]) => { dynamicRoutes.value = routes; }; return { menuData, dynamicRoutes, isRoutesInitialized, // 公開這個(gè)狀態(tài),方便其他地方判斷 setDynamicRoutes, getDynamicRoutes, setUpdatedDynamicRoutes, // 更新動(dòng)態(tài)路由的函數(shù) beforeRouter }; });
1.6.4 在src目錄下新建文件夾plugins,在該文件夾中新建文件element-plus.ts
/* Element-plus組件庫 */ import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import zhCn from 'element-plus/es/locale/lang/zh-cn' import { App } from 'vue' export default { install (app: App) { app.use(ElementPlus, { locale: zhCn }) } }
1.6.5 需要來配置main.ts,vite.config.ts以及tsconfig.json
1.6.5.1 main.ts配置
import { createApp } from "vue"; import App from "./App.vue"; import router from "./router/index"; import ElementPlus from "./plugins/element-plus"; import * as ElementPlusIconsVue from "@element-plus/icons-vue"; import { pinia } from '@/store/index'; // 導(dǎo)入 store // 創(chuàng)建 Pinia 實(shí)例 // 路由攔截 路由發(fā)生變化修改頁面title router.beforeEach((to, from, next) => { if (to.meta.title) { document.title = to.meta.title; } next(); }); const app = createApp(App); // // 自動(dòng)注冊(cè)全局組件 app.use(router).use(ElementPlus).use(pinia).mount("#app"); for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component); }
1.6.5.2 vite.config.ts配置
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' export default defineConfig({ plugins: [vue()], resolve: { alias: { // 設(shè)置別名 方便路徑引入 '@': path.resolve(__dirname, 'src'), } } })
1.6.5.3 tsconfig.json配置
{ "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "sourceMap": true, "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true, "paths": { "@/*": ["./src/*"] // 配置路徑別名,不做配置會(huì)報(bào)錯(cuò) } //就是這個(gè)沒有設(shè)置導(dǎo)致的 }, // "extends": "./tsconfig.extends.json", "include": ["src/**/*.tsx","src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] }
1.6.5.4 此外vue3在插件引入時(shí)有些時(shí)候會(huì)報(bào)錯(cuò)無法找到模塊“xxx”的聲明文件,此時(shí)需要在src目錄下新建一個(gè)env.d.ts文件
/// <reference types="vite/client" /> // 類型補(bǔ)充、環(huán)境變量 declare module "*.vue" { import type { DefineComponent } from "vue"; // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types const component: DefineComponent<{}, {}, any>; export default component; } // eslint-disable-next-line no-unused-vars interface ImportMetaEnv { readonly VITE_APP_TITLE: string; readonly VITE_API_BASEURL: string; // 更多環(huán)境變量... } // 如果遇到路徑缺失找不到的情況 // 無法找到模塊“xxx”的聲明文件,就將該模塊加入到下列代碼中進(jìn)行補(bǔ)充聲明 declare module "xxxx";
1.7 因?yàn)榭紤]是純前端模擬后端給的路由數(shù)據(jù)
所以我自己模擬一個(gè)json文件,需在public文件夾中新建dynamicRouter.json來存放模擬后端返回的路由數(shù)據(jù),后期從接口獲取可進(jìn)行更改
[ { "path": "/principle", "name": "principle", "component": "/principle/index", "meta": { "title": "Vue3響應(yīng)式原理" } }, { "path": "/ref", "name": "ref", "meta": { "title": "ref類" }, "children": [ { "path": "/ref/index", "name": "ref", "component": "/ref/common/ref", "meta": { "title": "ref" } }, { "path": "/ref/toRaw", "name": "toRaw", "component": "/ref/common/toRaw", "meta": { "title": "toRaw" } }, { "path": "/ref/toRef", "name": "toRef", "component": "/ref/common/toRef", "meta": { "title": "toRef" } }, { "path": "/ref/toRefs", "name": "toRefs", "component": "/ref/common/toRefs", "meta": { "title": "toRefs" } }, { "path": "/ref/isRef", "name": "isRef", "component": "/ref/no-common/isRef", "meta": { "title": "isRef" } }, { "path": "/ref/Ref", "name": "Ref", "component": "/ref/no-common/Ref", "meta": { "title": "Ref" } }, { "path": "/ref/shallowRef", "name": "shallowRef", "component": "/ref/no-common/shallowRef", "meta": { "title": "shallowRef" } }, { "path": "/ref/triggerRef", "name": "triggerRef", "component": "/ref/no-common/triggerRef", "meta": { "title": "triggerRef" } } ] } ]
如下是文件對(duì)應(yīng)的位置
到目前為止整體的環(huán)境已經(jīng)搭建完善,大概結(jié)構(gòu)如下
2. 在views文件夾下新建文件夾login
在其中新建文件index.vue
<template> <div class="login"> //登錄框 <div class="loginPart"> <h2>用戶登錄</h2> <el-form ref="ruleFormRef" :model="user" status-icon :rules="rules" label-width="100px" class="demo-ruleForm" style="transform: translate(-30px)" > <el-form-item label="賬號(hào):" prop="account" > <el-input v-model="user.account" placeholder="請(qǐng)輸入賬號(hào)" maxlength="20" clearable /> </el-form-item> <el-form-item label="密碼:" prop="password" > <el-input v-model="user.password" type="password" placeholder="請(qǐng)輸入密碼" maxlength="20" show-password clearable /> </el-form-item> <el-button class="btn" type="primary" @click="onSubmit(ruleFormRef)" > 登錄 </el-button> </el-form> </div> </div> </template> <script setup lang="ts"> import { reactive, ref, onMounted } from "vue"; //導(dǎo)入模擬的動(dòng)態(tài)路由數(shù)據(jù) import menuData from '/public/dynamicRouter.json'; // 導(dǎo)入動(dòng)態(tài)菜單數(shù)據(jù) import { ElMessage, type FormInstance } from "element-plus"; import { useRouter } from "vue-router"; import { initDynamicRoutes } from "@/router/dynamicRoutes"; // 導(dǎo)入初始化動(dòng)態(tài)路由的方法 import { useRouteStore, useUserStore} from "@/store"; const router = useRouter(); const routeStore = useRouteStore(); const UserStore = useUserStore(); type loginReq = { account: string; password: string; }; onMounted(() => {}); //from表單校驗(yàn) const ruleFormRef = ref<FormInstance>(); // 這里存放數(shù)據(jù) const user = reactive<loginReq>({ account: "admin", password: "123456", }); const users = reactive<loginReq>({ account: "admin", password: "123456", }); //校驗(yàn) const validatePassword = (rule: any, value: any, callback: any) => { if (value === "") { callback(new Error("請(qǐng)輸入密碼")); } else { callback(); } }; const validateAccount = (rule: any, value: any, callback: any) => { if (value === "") { callback(new Error("請(qǐng)輸入賬號(hào)")); } else { callback(); } }; //校驗(yàn) const rules = reactive({ password: [{ validator: validatePassword, trigger: "blur" }], account: [{ validator: validateAccount, trigger: "blur" }], }); const changeRegist = () => { router.replace("/regist"); }; const onSubmit = (formEl: FormInstance | undefined) => { if (!formEl) return; formEl.validate((valid) => { if (valid) { // 如果需要保存 token 或賬戶信息,存儲(chǔ)在全局狀態(tài) // 假設(shè)這里會(huì)調(diào)用登錄 API 返回一個(gè) token const token = "mock_token"; // 模擬登錄后返回的 token // 使用 Pinia 保存 token,并設(shè)置到 cookie 中 // 存儲(chǔ)在 cookie 中的 token 可以配合 httpOnly 和 Secure 標(biāo)志來增強(qiáng)安全性, // 這樣可以防止 XSS 攻擊并確保 token 只有在通過 HTTPS 協(xié)議時(shí)才會(huì)被發(fā)送 UserStore.setToken(token); ElMessage.success("登錄成功"); // 獲取菜單數(shù)據(jù) if(routeStore.isRoutesInitialized){ // 使用 nextTick 確保路由添加完成后再進(jìn)行跳轉(zhuǎn) nextTick(() => { // 跳轉(zhuǎn)到首頁或其他路由 router.push('/') // 假設(shè) 'home' 是你動(dòng)態(tài)路由中的一個(gè)頁面名稱 .then(() => { console.log('跳轉(zhuǎn)成功'); }) .catch((error) => { console.error('跳轉(zhuǎn)失敗', error); }); }); }else{ initDynamicRoutes(menuData) // 標(biāo)記路由已初始化 routeStore.isRoutesInitialized = true // 使用 nextTick 確保路由添加完成后再進(jìn)行跳轉(zhuǎn) nextTick(() => { // 跳轉(zhuǎn)到首頁或其他路由 router.push('/') // 假設(shè) 'home' 是你動(dòng)態(tài)路由中的一個(gè)頁面名稱 .then(() => { console.log('跳轉(zhuǎn)成功'); }) .catch((error) => { console.error('跳轉(zhuǎn)失敗', error); }); }); } } }) }; </script> <style scoped lang="scss"> .login { height: 100%; width: 100%; overflow: hidden; } .login__particles { height: 100%; width: 100%; background-size: cover; background-repeat: no-repeat; background-image: url("@/assets/0001.jpg"); opacity: 0.9; position: fixed; pointer-events: none; } h2 { margin: 0 0 30px; padding: 0; color: #fff; text-align: center; /*文字居中*/ } .btn { transform: translate(170px); width: 80px; height: 40px; font-size: 15px; } </style>
3. layout中制作動(dòng)態(tài)路由菜單
<!-- 自定義編輯的樣式不采用element-plus --> <template> <div class="app-container"> <header> <div class="menu"> <el-col :span="24"> <el-menu :router="true" active-text-color="#ffd04b" background-color="#545c64" class="el-menu-vertical-demo" text-color="#fff" @open="handleOpen" @close="handleClose" :unique-opened="true" :default-active="defaultPath" > <div v-for="(item, index) in menuList[0].children" :key="index" > <el-menu-item v-if="!item.children" :index="item.path" :route="item.path" > <!-- <el-icon><setting /></el-icon> --> <span>{{ item.meta?.title }}</span> </el-menu-item> <el-sub-menu v-if="item.children" :index="item.path" > <template #title> <!-- <el-icon><setting /></el-icon> --> <span>{{ item.meta?.title }}</span> </template> <el-menu-item-group> <el-menu-item v-for="(child, childIndex) in item.children" :key="childIndex" :index="child.path" > {{ child.meta?.title }} </el-menu-item> </el-menu-item-group> </el-sub-menu> </div> </el-menu> </el-col> </div> </header> <main> <router-view /> </main> </div> </template> <script lang="ts" setup> import { computed, onMounted } from "vue"; import { useRouteStore } from '@/store'; // 調(diào)整路徑為實(shí)際 Store 文件位置 import { useRouter } from "vue-router"; const router = useRouter() const routeStore = useRouteStore(); const menuList = computed(() => routeStore.dynamicRoutes); const defaultPath = ref<string>('') const handleOpen = (key: string, keyPath: string[]) => { console.log('key', key); console.log('keyPathpen', keyPath); }; const handleClose = (key: string, keyPath: string[]) => { console.log('close', key, keyPath); }; onMounted(() => { defaultPath.value = routeStore.beforeRouter }); </script> <style lang="scss" scoped> * { margin: 0; padding: 0; box-sizing: border-box; /* 確保所有元素都遵循邊框盒模型 */ touch-action: none; } .app-container { display: flex; /* 使用 flexbox 創(chuàng)建左右布局 */ justify-content: space-between; height: 100vh; /* 設(shè)置容器高度為視口高度 */ background-color: #fff; } header { width: 150px; /* 固定寬度,左側(cè)菜單欄寬度,可以根據(jù)需要調(diào)整 */ // background-color: #f8f9fa; /* 設(shè)置背景顏色 */ box-shadow: 2px 0px 10px rgba(0, 0, 0, 0.1); /* 給左側(cè)菜單欄添加陰影 */ overflow-y: auto; /* 左側(cè)菜單支持垂直滾動(dòng) */ height: 100vh; /* 使 header 高度占滿整個(gè)屏幕 */ .el-menu { height: 100vh; /* 高度占滿全屏 */ width: 100%; /* 設(shè)置菜單寬度為220px,避免過窄 */ font-size: 0.175rem; font-weight: bold; color: #fff; overflow-y: scroll; /* 確保菜單項(xiàng)寬度一致 */ .el-menu-item, .el-sub-menu { width: 100%; /* 確保菜單項(xiàng)和子菜單項(xiàng)的寬度自適應(yīng) */ // padding-left: 0.25rem; /* 為每個(gè)菜單項(xiàng)增加左側(cè)的內(nèi)邊距 */ } /* 一級(jí)菜單項(xiàng) */ .el-menu-item { padding: 10px 20px; /* 設(shè)置內(nèi)邊距,避免菜單項(xiàng)過于擁擠 */ text-align: left; /* 左對(duì)齊文本 */ font-size: 0.175rem; /* 調(diào)整字體大小 */ } /* 二級(jí)菜單項(xiàng)(子菜單) */ .el-sub-menu { // padding-left: 10px; /* 為二級(jí)菜單增加縮進(jìn) */ background-color: #434d56; /* 給二級(jí)菜單背景設(shè)置一個(gè)較深的顏色 */ /* 子菜單項(xiàng)的縮進(jìn) */ .el-menu-item { padding-left: 1rem; /* 設(shè)置二級(jí)菜單項(xiàng)的縮進(jìn),區(qū)別于一級(jí)菜單 */ } } /* 子菜單的展開箭頭樣式 */ :deep(.el-sub-menu__icon-arrow) { color: #ffd04b; /* 設(shè)置箭頭顏色為黃色 */ } /* 設(shè)置展開狀態(tài)時(shí),子菜單的背景色變化 */ .el-sub-menu.is-opened { background-color: #3a424a; /* 打開時(shí)的背景色 */ } /* 設(shè)置菜單項(xiàng)和子菜單項(xiàng)的 hover 狀態(tài) */ .el-menu-item:hover, .el-sub-menu:hover { background-color: #333c44; /* 鼠標(biāo)懸浮時(shí)的背景色 */ } /* 設(shè)置當(dāng)前激活的菜單項(xiàng)的背景顏色 */ .el-menu-item.is-active { background-color: #ff6600; /* 激活狀態(tài)的背景色 */ } } .el-menu::-webkit-scrollbar{ display: none; } /* 自定義子菜單圖標(biāo)大小 */ .el-menu-item .el-icon, .el-sub-menu .el-icon { font-size: 1.2rem; /* 調(diào)整圖標(biāo)的大小 */ margin-right: 0.5rem; /* 給圖標(biāo)增加右側(cè)的間距 */ } :deep(.el-sub-menu__title){ width: 100%!important; } .el-menu-item span, .el-sub-menu span { font-size: 10px; /* 設(shè)置文本的字體大小 */ font-weight: bold; /* 設(shè)置文本加粗 */ } :deep(.el-sub-menu__icon-arrow){ left: 50px!important; } } main { display: flex; flex: 1; /* main 占據(jù)剩余空間 */ // margin-left: 250px; /* 給 main 留出與 header 相同的空間 */ padding: 20px; overflow-y: auto; /* 支持內(nèi)容區(qū)域滾動(dòng) */ background-image: linear-gradient(135deg, #102363, #346177, #3fa489, #34ec98); flex-direction: column; } header::-webkit-scrollbar, main::-webkit-scrollbar { display: none; /* 隱藏滾動(dòng)條 */ } /* 小屏幕適配 */ @media (max-width: 768px) { .app-container { flex-direction: column; /* 在小屏幕下轉(zhuǎn)換為上下布局 */ } header { width: 100%; /* 屏幕小于768px時(shí),左側(cè)菜單占滿全寬 */ position: relative; /* 取消固定定位,方便移動(dòng) */ height: auto; /* 自動(dòng)高度 */ } main { margin-left: 0; /* 小屏幕時(shí)不需要左側(cè)留白 */ } } </style>
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
手把手教你vue實(shí)現(xiàn)動(dòng)態(tài)路由
動(dòng)態(tài)路由可以根據(jù)不同用戶登錄獲取不一樣的路由層級(jí),可隨時(shí)調(diào)配路由,下面這篇文章主要給大家介紹了關(guān)于vue實(shí)現(xiàn)動(dòng)態(tài)路由的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07vue項(xiàng)目運(yùn)行或打包時(shí),頻繁內(nèi)存溢出情況問題
這篇文章主要介紹了vue項(xiàng)目運(yùn)行或打包時(shí),頻繁內(nèi)存溢出情況的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04Vue項(xiàng)目本地沒有問題但部署到服務(wù)器上提示錯(cuò)誤(問題解決方案)
一個(gè) VUE 的項(xiàng)目在本地部署沒有問題,但是部署到服務(wù)器上的時(shí)候提示訪問資源的錯(cuò)誤,遇到這樣的問題如何解決呢?下面小編給大家?guī)砹薞ue項(xiàng)目本地沒有問題但部署到服務(wù)器上提示錯(cuò)誤的解決方法,感興趣的朋友一起看看吧2023-05-05Vue中使用 Echarts5.0 遇到的一些問題(vue-cli 下開發(fā))
這篇文章主要介紹了Vue中使用 Echarts5.0 遇到的一些問題(vue-cli 下開發(fā)),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10利用Vue+ElementUi實(shí)現(xiàn)評(píng)論功能
這篇文章主要介紹了如何利用Vue+ElementUi實(shí)現(xiàn)評(píng)論功能,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-04-04vue中實(shí)現(xiàn)子組件相互切換且數(shù)據(jù)不丟失的策略詳解
項(xiàng)目為數(shù)據(jù)報(bào)表,但是一個(gè)父頁面中有很多的子頁面,而且子頁面中不是相互關(guān)聯(lián),但是數(shù)據(jù)又有聯(lián)系,所以本文給大家介紹了vue中如何實(shí)現(xiàn)子組件相互切換,而且數(shù)據(jù)不會(huì)丟失,并有詳細(xì)的代碼供大家參考,需要的朋友可以參考下2024-03-03教你如何在 Nuxt 3 中使用 wavesurfer.js
這篇文章主要介紹了如何在 Nuxt 3 中使用 wavesurfer.js,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-01-01vue獲取DOM元素并設(shè)置屬性的兩種實(shí)現(xiàn)方法
下面小編就為大家?guī)硪黄獀ue獲取DOM元素并設(shè)置屬性的兩種實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09