Vue 3 + Vite + Element Plus 的企业级管理平台

后台管理办公系统

 

 

一. 创建项目

1.1 项目概述

办公系统 是一个基于 Vue 3 + Vite + Element Plus 的企业级管理平台,旨在提升公司员工的办公效率和管理便捷性。该系统采用 动态路由、动态面包屑导航、多页签导航,提供灵活的界面交互和高效的数据管理。

1.2 核心技术栈

  • Vue 3:采用 Composition API 进行组件化开发,提升代码复用性和可维护性。

  • Vite:作为前端构建工具,提供极速冷启动、热更新和高效打包能力。

  • Element Plus:基于 Vue 3 的 UI 组件库,提供美观、易用的界面组件。

  • 动态路由:基于 vue-router,支持权限控制,动态加载页面,提升系统扩展性。

  • 动态面包屑:根据当前路由自动生成导航路径,提升用户体验和操作便捷性。

  • 多页签导航:支持多个页面同时打开,并可自由切换,提高操作效率。

 

1.2 vite创建项目

​
"""
ningcaichen@bogon 04-北京地铁内部开发平台 % npm create vue@latest
​
> npx
> create-vue
​
​
Vue.js - The Progressive JavaScript Framework
​
✔ 请输入项目名称: … vue_web_v1
✔ 是否使用 TypeScript 语法? … 否 / 是
✔ 是否启用 JSX 支持? … 否 / 是
✔ 是否引入 Vue Router 进行单页面应用开发? … 否 / 是
✔ 是否引入 Pinia 用于状态管理? … 否 / 是
✔ 是否引入 Vitest 用于单元测试? … 否 / 是
✔ 是否要引入一款端到端(End to End)测试工具? › 不需要
✔ 是否引入 ESLint 用于代码质量检测? › 否
​
正在初始化项目 /Users/ningcaichen/Documents/02-python相关文档/04-北京地铁内部开发平台/vue_web_v1...
​
项目初始化完成,可执行以下命令:
​
  cd vue_web_v1
  npm install
  npm run dev
​
ningcaichen@bogon 04-北京地铁内部开发平台 % cd vue_web_v1 
ningcaichen@bogon vue_web_v1 %  npm install
​
"""
​
​

 

1.3 项目初始化

​
​
"""
01 删除自带的组件,清空路由和样式
​
02 按照工具 vite-plugin-vue-setup-extend 
​
​
"""
​
#01 下载依赖`
npm i vite-plugin-vue-setup-extend -D
​
#02 导入包 vite.config.ts
import VueSetupExtend from 'vite-plugin-vue-setup-extend'     //导入
​
export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
    VueSetupExtend(),                                         //注册
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    },
  },
})
​
​
#03 使用该模块
​
  
<script lang="ts" setup name="Person222">           //使用 name属性直接定义           
  // 直接定义数据即可,会自动帮你返回
    let name = '果果'
​
  
</script>
​
​

1.4 创建路由组件

​
​
// 主要是书写 /hom组件
​
import {createRouter, createWebHistory} from 'vue-router'
​
​
import Home from '@/views/Home.vue';
​
const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
​
        // 重定向到home
        {
            path: '/',
            redirect: '/home'
        },
​
        {
            name: 'home',
            path: '/home',
            component: Home,
        },
​
​
    ],
})
​
export default router
​

 

1.5 引用element UI

 

​
​
#01 安装
npm install element-plus --save
​
#02 全局引用
import './assets/main.css'
​
import { createApp } from 'vue'
import { createPinia } from 'pinia'
​
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
​
import App from './App.vue'
import router from './router'
​
const app = createApp(App)
​
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
​
app.mount('#app')
​
​

 

二. 书写基本组件

​
#01 基本目录如下
ningcaichen@bogon vue_web_v1 % tree src
src
├── App.vue                       # 组件入口 
├── assets  
│   ├── base.css
│   ├── home.css                  # home主页样式
│   ├── logo.svg
│   └── main.css
├── components                    # 组件目录
│   └── layout
│       ├── Breadcrumb.vue        # 面包屑组件
│       ├── MainLayout.vue        # 空组件:用于加载路由内容
│       ├── Sidebar.vue           # 左侧菜单组件
│       └── Tags.vue              # 多页签导航组件
├── main.ts                       # 主配置文件
├── router            
│   └── index.ts                  # 路由配置文件
├── stores
│   ├── counter.ts                # pinia 默认存储store
│   └── tabs.ts                   # 多页签导航状态存储文件       
├── utils
│   ├── commonData.ts             # 菜单数据
│   └── icons.ts                  # 图标数据
└── views                         # 路由文件
    ├── Home.vue
    ├── Index.vue
    ├── Login.vue
    ├── admin
    │   ├── permission
    │   │   └── Permission.vue
    │   └── role
    │       └── Role.vue
    └── system
        ├── dept
        │   └── Dept.vue
        ├── station
        │   └── Station.vue
        └── user
            └── User.vue
​
​
​

 

 

2.1 定义动态菜单数据

src/utils/commonData.ts

​
​
​
​
// 定义接口规范
interface MenuItem {
    id: number
    authName: string
    icon: string
    path: string | null
    children: MenuItem[]
}
// 路由数据
export const menuData: { data: MenuItem[]; meta: any } = {
    data: [
        {
            id: 100,
            authName: "人员管理",
            icon: "Avatar",
            path: '/home/personnel',
            children: [
                { id: 100003, authName: "用户管理", path: "/home/personnel/user", icon: "Menu", children: [] },
                { id: 100001, authName: "岗位管理", path: "/home/personnel/station", icon: "Pointer", children: [] },
                { id: 100002, authName: "部门管理", path: "/home/personnel/dept", icon: "EditPen", children: [] }
            ]
        },
        {
            id: 102,
            authName: "系统管理",
            icon: "HomeFilled",
            path: '/home/admin',
            children: [
                { id: 102003, authName: "角色管理", path: "/home/admin/role", icon: "Food", children: [] },
                { id: 102001, authName: "权限管理", path: "/home/admin/permission", icon: "Opportunity", children: [] }
            ]
        }
    ],
    meta: {
        msg: "获取菜单成功",
        status: 200
    }
}
​
​
​

2.2 定义路由数据

router/index.ts

import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
​
​
​
// 人员管理模块
import User from '@/views/system/user/User.vue'
import Station from '@/views/system/station/Station.vue'
import Dept from '@/views/system/dept/Dept.vue'
​
// 系统管理模块
import Role from '@/views/admin/role/Role.vue'
import Permission from '@/views/admin/permission/Permission.vue'
​
// 布局组件
const MainLayout = () => import('@/components/layout/MainLayout.vue')
​
​
​
const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        {
            path: '/',
            redirect: '/home',
            meta: { hidden: true ,closable: true }
        },
        {
            path: '/home',
            name: 'Home',
            component: Home,
            meta: { title: '首页',closable: true  },
            // redirect: '/home/system', // 默认重定向到系统管理
            children: [
                // 人员管理子模块
                {
                    path: 'personnel',
                    name: 'Personnel',
                    component: MainLayout,
                    meta: { title: '人员管理',closable: true  },
                    children: [
                        { path: 'user', name: 'User', component: User, meta: { title: '用户管理',closable: true  } },
                        { path: 'station', name: 'Station', component: Station, meta: { title: '岗位管理',closable: true  } },
                        { path: 'dept', name: 'Dept', component: Dept, meta: { title: '部门管理' ,closable: true } }
                    ]
                },
                // 角色权限子模块
                {
                    path: 'admin',
                    name: 'Admin',
                    component: MainLayout,
                    meta: { title: '系统管理' ,closable: true },
                    children: [
                        { path: 'role', name: 'Role', component: Role, meta: { title: '角色管理',closable: true  } },
                        { path: 'permission', name: 'Permission', component: Permission, meta: { title: '权限管理',closable: true  } }
                    ]
                }
            ]
        }
    ]
})
​
export default router
​
  • 注意这里引用一个空组件 后续会引用

​
<!-- src/layout/MainLayout.vue -->
<template>
  <div class="main-layout">
    <router-view />
  </div>
</template>
​
<script setup lang="ts">
// 空组件,仅用于路由嵌套
</script>
​

 

 

 

2.3 Home.vue 主页面布局

我们使用el-menu 组件创建布局

src/views/Home.vue

<template>
  <div class="system-container">
    <el-container class="main-wrapper">
​
​
      <!-- 左侧导航区域 Sidebar组件 -->
      <Sidebar
          :is-collapse="isCollapse"
          :menuData="menu_list.data"
          :active-menu-index="activeMenuIndex"
      />
​
      <!-- 右侧内容区 -->
      <el-container>
        <el-header class="operate-header">
          <div class="header-left">
            <el-button
                link
                class="collapse-btn"
                @click="toggleSidebar"
            >
              <el-icon :size="20">
                <component :is="collapseIcon"/>
              </el-icon>
            </el-button>
​
​
            <!--面包屑组件区域-->
            <div class="breadcrumb-container">
              <el-breadcrumb separator="/">
                <Breadcrumb/>
              </el-breadcrumb>
            </div>
          </div>
          <div class="toolbar">
            <el-button type="primary" plain>退出系统</el-button>
          </div>
        </el-header>
​
        <el-main>
          <div class="content-card">
            <div class="table-container">
              <RouterView/>
            </div>
          </div>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>
​
<script setup lang="ts">
import {ref, computed} from 'vue'
import {Fold, Expand} from '@element-plus/icons-vue'
import {menuData} from '@/utils/commonData'
import {useRoute} from "vue-router";
​
import Sidebar from "@/components/layout/Sidebar.vue";
import Breadcrumb from "@/components/layout/Breadcrumb.vue";
​
const route = useRoute();
​
​
// 侧边栏状态
const isCollapse = ref(false)
const toggleSidebar = () => {
  isCollapse.value = !isCollapse.value
}
​
// 菜单数据
const activeMenuIndex = ref('100')
const menu_list = menuData // 直接使用已响应式的数据
​
// 折叠图标计算属性
const collapseIcon = computed(() => isCollapse.value ? Expand : Fold)
</script>
​
<style scoped>
@import '@/assets/home.css';
</style>
​
​

2.4 添加css样式

src/assets/home.css

​
/* 系统容器 */
.system-container {
    height: 100vh;
    --sidebar-bg: #304156;
    --header-bg: #ffffff;
    overflow: hidden;
}
​
/* 主布局 */
.main-wrapper {
    height: 100%;
}
​
/* 侧边栏区域 */
.nav-sidebar {
    background: var(--sidebar-bg);
    transition: width 0.3s ease;
}
​
/* 系统LOGO */
.system-logo {
    height: 60px;
    display: flex;
    align-items: center;
    padding: 0 20px;
    border-bottom: 1px solid rgba(255,255,255,0.1);
    transition: padding 0.3s;
}
​
.logo-text {
    color: #fff;
    font-size: 18px;
    font-weight: 600;
    letter-spacing: 1px;
    white-space: nowrap;
}
​
.logo-collapse {
    padding: 0 10px;
    justify-content: center;
}
​
/* 折叠按钮 */
.collapse-btn {
    color: #b0bac5;
    margin-right: 12px;
    transition: 0.3s;
}
​
.collapse-btn:hover {
    color: #fff;
    background-color: rgba(255,255,255,0.1);
}
​
/* 菜单深度样式 */
:deep(.sidebar-menu) {
    border-right: none !important;
}
​
:deep(.el-menu-item),
:deep(.el-sub-menu__title) {
    height: 46px;
    line-height: 46px;
    transition: all 0.3s;
}
​
:deep(.el-menu-item:hover),
:deep(.el-sub-menu__title:hover) {
    background-color: rgba(255,255,255,0.08) !important;
}
​
:deep(.el-menu-item.is-active) {
    background: rgba(64, 158, 255, 0.1) !important;
    border-left: 3px solid #409EFF;
}
​
.menu-icon {
    margin-right: 8px;
    font-size: 18px;
}
​
/* 折叠状态菜单 */
:deep(.el-menu--collapse) {
    border-right: none;
}
​
:deep(.el-menu--collapse .el-sub-menu > .el-menu) {
    display: none;
}
​
:deep(.el-menu--collapse .el-sub-menu__title span),
:deep(.el-menu--collapse .el-menu-item span) {
    display: none;
}
​
/* 头部区域 */
.operate-header {
    height: 56px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: var(--header-bg);
    border-bottom: 1px solid #e4e7ed;
    padding: 0 24px;
}
​
.header-left {
    display: flex;
    align-items: center;
    gap: 12px;
}
​
.el-breadcrumb {
    font-size: 15px;
}
​
/* 内容区域 */
.content-card {
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.05);
    padding: 16px;
}
​
.table-container {
    margin-top: 12px;
}
​

 

 

2.5 左侧导航菜单组件

src/components/layout/Breadcrumb.vue

​
<template>
  <el-aside
      :width="isCollapse ? '64px' : '240px'"
      class="nav-sidebar"
  >
    <div class="system-logo" :class="{ 'logo-collapse': isCollapse }">
      <span v-if="!isCollapse" class="logo-text">易创盈办公系统</span>
      <span v-else class="logo-text">易</span>
    </div>
​
    <el-menu
        :default-active="activeMenuIndex"
        class="sidebar-menu"
        background-color="#304156"
        text-color="#b0bac5"
        active-text-color="#fff"
        :collapse="isCollapse"
        :collapse-transition="false"
        :router="true"
    >
​
      <!--      一级菜单-->
      <el-sub-menu
          v-for="item in menuData"
          :key="item.id"
          :index="String(item.id)"
      >
        <template #title>
          <el-icon class="menu-icon">
            <component :is="getIcon(item.icon)"/>
          </el-icon>
          <span>{{ item.authName }}</span>
        </template>
​
​
        <el-menu-item
            v-for="child in item.children"
            :key="child.id"
            :index="String(child.path)"
        >
          <el-icon>
            <component :is="getIcon(child.icon)"/>
          </el-icon>
          <span>{{ child.authName }}</span>
        </el-menu-item>
      </el-sub-menu>
    </el-menu>
  </el-aside>
</template>
​
<script setup lang="ts">
​
defineProps<{
  isCollapse: boolean
  menuData: any[]
  activeMenuIndex: string
}>()
​
​
import {getIcon} from '@/utils/icons'
</script>
​
​
<style scoped>
@import '@/assets/home.css';
</style>
​

2.5 动态图标组件 icons

src/utils/icons.ts

​
​
import {
    User, Avatar, Pointer, Menu,
    EditPen, Tools, HomeFilled, Food,
    Opportunity, Fold, Expand
} from '@element-plus/icons-vue'
​
// 类型定义
export type IconKey = keyof typeof iconComponents
​
// 图标映射对象(使用 as const 锁定类型)
export const iconComponents = {
    User,
    Avatar,
    Pointer,
    Menu,
    EditPen,
    Tools,
    HomeFilled,
    Food,
    Opportunity,
    Fold,
    Expand
} as const
​
// 获取图标函数(带安全类型)
export const getIcon = (iconName: string) => {
    const key = iconName as IconKey
    return iconComponents[key] || User  // 默认返回User图标
}
​

2.6 动态面包屑组件

src/components/layout/Breadcrumb.vue

<!-- src/components/common/Breadcrumb.vue -->
<template>
  <!-- 动态生成面包屑项 -->
  <el-breadcrumb separator="/">
​
    <!--  面包屑组件-->
    <!-- :to="item.path"    绑定跳转路径,如截图中可点击的导航 -->
    <el-breadcrumb-item
        v-for="(item, index) in breadcrumbItems"
        :key="index"
        :to="item.path"
    >
      <!-- 显示标题,如图中的"人员管理" -->
      {{ item.title }}
​
    </el-breadcrumb-item>
​
​
  </el-breadcrumb>
</template>
​
<script setup lang="ts">
import {computed} from 'vue'
import {useRoute} from 'vue-router'
import {menuData} from '@/utils/commonData' // 导入菜单数据(如截图左侧导航数据)
​
const route = useRoute() // 获取当前路由信息
​
// 核心计算属性:生成面包屑导航数据
const breadcrumbItems = computed(() => {
  const matched = route.matched
      .filter(item => !item.meta?.hidden)
      .map(item => {
        const titleSource = item.meta?.title ? '路由' : '菜单'
        const finalTitle = item.meta?.title || findMenuTitle(item.path)
​
        // 调试信息(与截图中的控制台日志格式匹配)
        console.log(
            `[面包屑来源] 路径: ${item.path.padEnd(25)} | 来源: ${titleSource.padEnd(4)} | 标题: ${finalTitle}`
        )
​
        return {
          path: item.path,
          title: finalTitle
        }
      })
​
  return matched.length ? matched : [{ path: '/home', title: '首页' }]
})
​
// 调试输出(对应截图底部控制台的日志输出)
console.log('当前面包屑数据:', breadcrumbItems.value)
​
// 从菜单数据查找标题,如果路由未设置 则从菜单里查找
const findMenuTitle = (path: string) => {
  // 扁平化菜单数据结构(包含多级菜单)
  const flatMenu = menuData.data.flatMap(item => [
    item,
    ...(item.children || []) // 展开子菜单(如"用户管理"展开到同级)
  ])
​
  // 查找匹配路径的菜单项
  return flatMenu.find(menu => menu.path === path)?.authName || path // 默认显示路径
}
​
​
</script>
​
​
​
  • 图示

 

2.7 多页签导航

(1) Pinia 定义数据和方法

stores/tabs.tx

​
​
​
import { defineStore } from 'pinia'
​
// 定义标签的数据结构
export interface Tab {
    title: string // 标签页名称
    path: string  // 标签页路由路径
    closable?: boolean // 是否可关闭(首页不能关闭)
}
​
// 定义 Pinia Store
export const useTabsStore = defineStore('tabs', {
    // state:存储数据
    state: () => ({
        // 初始化 tabs,默认包含首页,且首页不可关闭
        tabs: [{ title: '首页', path: '/home', closable: false }] as Tab[],
​
        // 当前激活的标签路径,默认激活首页
        activeTab: '/home'
    }),
​
    // actions:定义修改 state 的方法
    actions: {
        // 添加标签
        addTab(tab: Tab) {
            // 先检查这个标签是否已存在
            const exists = this.tabs.find(t => t.path === tab.path)
            if (!exists) {
                // 设置是否可关闭,首页不能被关闭
                tab.closable = tab.path === '/home' ? false : true
                this.tabs.push(tab) // 添加到 tabs 列表
            }
            // 设置当前激活的标签
            this.activeTab = tab.path
        },
​
        // 删除标签
        removeTab(path: string) {
            if (path === '/home') return // 首页不能被删除
​
            const index = this.tabs.findIndex(t => t.path === path)
            if (index > -1) {
                this.tabs.splice(index, 1) // 从 tabs 中移除
​
                // 如果删除的是当前激活的标签,调整 activeTab
                if (this.activeTab === path) {
                    // 如果被删除的标签后面还有标签,则激活下一个标签
                    if (index < this.tabs.length) {
                        this.activeTab = this.tabs[index].path
                    }
                    // 否则激活前一个标签
                    else if (index - 1 >= 0) {
                        this.activeTab = this.tabs[index - 1].path
                    }
                    // 如果删除后 tabs 为空,则回到首页
                    else {
                        this.activeTab = '/home'
                    }
                }
            }
        },
​
        // 设置当前激活的标签
        setActiveTab(path: string) {
            this.activeTab = path
        }
    },
​
    // Pinia 持久化存储,保证刷新页面后状态不会丢失
    persist: {
        key: 'tabsStore', // 存储的 key
        storage: localStorage, // 存储方式(localStorage 或 sessionStorage)
        paths: ['tabs', 'activeTab'] // 需要持久化的字段
    } as any // 这里使用 `as any` 避免 TypeScript 类型错误
})
​
​

(2) Tags 组件

src/components/layout/Tags.vue

​
​
<template>
  <!-- Element Plus 的标签页组件 -->
  <!--
   绑定当前激活的标签页
   监听关闭标签页的事件
   允许手动关闭标签
   -->
​
  <el-tabs
      v-model="activeTab"
  type="card"
  @tab-remove="handleTabRemove"
  closable
  >
  <!--
    遍历所有标签页
    设定唯一的 key,提升渲染性能
    设定标签页的唯一标识符
    显示的标签名称
    控制是否可关闭(首页不可关闭)
​
  -->
  <el-tab-pane
      v-for="tab in tabs"
      :key="tab.path"
  :name="tab.path"
  :label="tab.title"
  :closable="tab.closable"
  >
  </el-tab-pane>
  </el-tabs>
</template>
​
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useTabsStore } from '@/stores/tabs'
​
const router = useRouter()  // 获取 Vue Router 实例
const tabsStore = useTabsStore()  // 获取标签页的状态管理
​
// 计算属性:获取当前所有的标签页数据
const tabs = computed(() => tabsStore.tabs)
​
// 计算属性:当前激活的标签页,支持双向绑定
const activeTab = computed({
  get: () => tabsStore.activeTab,  // 获取当前激活的标签页路径
  set: (val: string) => {
    tabsStore.setActiveTab(val)  // 更新 Pinia 中的状态
    router.push(val)  // 切换路由
  }
})
​
// 处理标签页的关闭事件
const handleTabRemove = (targetName: string) => {
  tabsStore.removeTab(targetName)  // 调用 store 方法删除标签
  router.push(tabsStore.activeTab)  // 切换到删除后的激活标签
}
</script>
​
<style scoped>
/* 整体 Tabs 容器样式 */
.el-tabs {
  background-color: #fff;               /* 背景颜色:白色 */
  border: 1px solid #ebeef5;            /* 浅灰色边框 */
  border-radius: 4px;                   /* 圆角边框 */
  margin: 1px 0;                        /* 上下外边距 */
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); /* 添加柔和阴影 */
}
​
/* 标签页头部样式 */
.el-tabs__header {
  background-color: #fff;               /* 头部背景保持白色 */
  border-bottom: 1px solid #ebeef5;      /* 添加底部边框 */
  padding: 0 10px;                      /* 左右内边距 */
}
​
/* 单个标签页内容区域样式 */
.el-tab-pane {
  font-size: 14px;                      /* 统一字体大小 */
  padding: 2px;                         /* 内边距调整 */
}
</style>
​
​

(3) 添加监听器

home.vue 文件中添加监听器

​
<template>
  <div class="system-container">
    <el-container class="main-wrapper">
      <!-- 左侧导航区域 Sidebar组件 -->
      <Sidebar
          :is-collapse="isCollapse"
          :menuData="menu_list.data"
          :active-menu-index="activeMenuIndex"
      />
​
      <!-- 右侧内容区 -->
      <el-container>
        <el-header class="operate-header">
          <div class="header-left">
            <el-button link class="collapse-btn" @click="toggleSidebar">
              <el-icon :size="20">
                <component :is="collapseIcon"/>
              </el-icon>
            </el-button>
            <!-- 面包屑组件区域 -->
            <div class="breadcrumb-container">
              <el-breadcrumb separator="/">
                <Breadcrumb/>
              </el-breadcrumb>
            </div>
          </div>
          <div class="toolbar">
            <el-button type="primary" plain>退出系统</el-button>
          </div>
        </el-header>
​
        <!-- 多页签导航组件 -->
        <Tags />
​
        <!-- 主要内容区域 -->
        <el-main>
          <div class="content-card">
            <div class="table-container">
              <RouterView/>
            </div>
          </div>
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>
​
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Fold, Expand } from '@element-plus/icons-vue'
import { menuData } from '@/utils/commonData'
import { useRoute } from 'vue-router'
​
import Sidebar from '@/components/layout/Sidebar.vue'
import Breadcrumb from '@/components/layout/Breadcrumb.vue'
import Tags from '@/components/layout/Tags.vue'
​
// 引入 Pinia 页签 store
import { useTabsStore } from '@/stores/tabs'
​
const route = useRoute()
const tabsStore = useTabsStore()
​
// 监听路由变化,自动添加页签(首页处理特殊)
watch(
    () => route.path,
    (newPath) => {
      let title: string
      if (newPath === '/home') {
        title = '首页'
      } else {
        title = typeof route.meta.title === 'string' ? route.meta.title : '无标题'
      }
      tabsStore.addTab({ title, path: newPath })
    },
    { immediate: true }
)
​
// 侧边栏状态
const isCollapse = ref(false)
const toggleSidebar = () => {
  isCollapse.value = !isCollapse.value
}
​
// 菜单数据
const activeMenuIndex = ref('100')
const menu_list = menuData
​
// 折叠图标计算属性
const collapseIcon = computed(() => isCollapse.value ? Expand : Fold)
</script>
​
<style scoped>
@import '@/assets/home.css';
</style>
​
​

 

(4) 修改样式

​
​
 /* home.css 文件内添加 */
​
.el-main {
    --el-main-padding: 10px;      /* 边距 */
    box-sizing: border-box;
    display: block;
    flex: 1;
    flex-basis: auto;
    overflow: auto;
    padding: var(--el-main-padding);
}
​

 

 

 

2.8 持久化存储

​
​
#01 下载 pinia-plugin-persistedstate
npm install pinia-plugin-persistedstate
​
#02 main.ts 导入
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
​
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
​
const app = createApp(App)
app.use(pinia)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
​
#03 pinia增加配置
export const useTabsStore = defineStore('tabs', {
​
    // Pinia 持久化存储,保证刷新页面后状态不会丢失
    persist: {
        key: 'tabsStore', // 存储的 key
        storage: localStorage, // 存储方式(localStorage 或 sessionStorage)
        paths: ['tabs', 'activeTab'] // 需要持久化的字段
    } as any // 这里使用 `as any` 避免 TypeScript 类型错误
  
  
})
​
​

 

 

 

 

 

 

 

 

posted @ 2025-03-10 17:45  宁采臣open  阅读(1249)  评论(0)    收藏  举报