Bootstrap

使用vue3从0开发一个后台管理系统

项目简介

源码地址

https://gitee.com/szxio/zx-vue-next

描述

本项目是使用 vue3 来开发的后台管理系统模板。页面简单大方,使用悬浮式的风格,将菜单栏,顶部面包屑,中间操作区域等合理划分,功能丰富,支持主题颜色自定义,一键开启黑色主题,浅色、深色菜单动态切换等。路由采用动态路由,依托若依后端接口,拥有强大的权限管理功能。对若依感兴趣的点此跳转,希望各位小伙伴能够在学习本项目的过程中或多或少的有所收获。

如果感觉对你有所帮助,请点击 Star,感谢支持。

本文档同步至以下网站:

  • https://songzx0106.github.io/
  • https://blog.csdn.net/SongZhengxing_?type=blog

技术栈

  • vue3
  • element-plus
  • Pinia
  • vue-router
  • js-cookie
  • sass

页面截图

8558467.jpg
image-20231101140753919
8558467.jpg

8558467.jpg

8558467.jpg

本地运行

本项目后端借用了若依的后台框架,在她的基础上稍作了修改。

可以在本地启动本项目中的 java 代码。再启动前端查看效果。

若依启动成功截图

8558467.jpg
前端项目运行

npm install

npm run dev

输入默认的账号密码

账号:admin
密码:admin123

创建项目

使用 vite 来创建我们的工程

npm create vite@latest

或者

yarn create vite

然后按照提示操作即可!

elementui-plus

安装

官方文档:https://element-plus.gitee.io/zh-CN/

安装

cnpm install element-plus --save

引入

// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'

const app = createApp(App)

app.use(ElementPlus)
app.mount('#app')

组件自动导入

实现组件自动导入

npm install -D unplugin-vue-components unplugin-auto-import

然后把下列代码插入到你的 Vite 的配置文件中

// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

注册所有图标

安装

# NPM
$ npm install @element-plus/icons-vue
# Yarn
$ yarn add @element-plus/icons-vue
# pnpm
$ pnpm install @element-plus/icons-vue

注册所有组件

// main.ts

import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

解决默认是英文的问题

引入中文包:import lang from 'element-plus/lib/locale/lang/zh-cn',然后设置全局语言即可

import lang from 'element-plus/lib/locale/lang/zh-cn'
import ElementPlus from 'element-plus'

const app = createApp(App)
app.use(ElementPlus, {
  locale: lang,
})
app.mount('#app')

添加Router

官方文档:https://router.vuejs.org/zh/

安装

npm install vue-router@4

新建测试路由

// 1.从vue-router导出两个方法使用
import {createRouter, createWebHashHistory} from 'vue-router';

// 2.声明菜单数组
const routes = [
    {
        path: '/',
        component: import("../view/home/home.vue")
    },
    {
        path: '/about',
        component: import("../view/about/about.vue")
    },
]

// 3. 创建路由实例并传递 `routes` 配置
// 你可以在这里输入更多的配置,但我们在这里
// 暂时保持简单
const router = createRouter({
    // 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
    history: createWebHashHistory(),
    routes, // `routes: routes` 的缩写
})

// 导出路由
export default router;

引入

// main.ts
import router from "./router/index"

const app = createApp(App)
app.use(router)
app.mount('#app')

修改App.vue

<template>
  <router-view/>
</template>

配置@路径别名

首先安装依赖

npm install @types/node
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

//设置路径别名
const alias = {
  '@': resolve(__dirname, './src'),
  '*': resolve(''),
}

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        vue()
    ],
    resolve: {alias},
})

安装prettier代码格式化插件

安装

npm i --save-dev prettier

然后再根目录新建 .prettierrc 文件,内容如下

{
  "printWidth": 180,
  "tabWidth": 4,
  "semi": false,
  "singleQuote": true,
  "trailingComma": "es5"
}

然后以 webStorm 工具为例,使用 prettier

8558467.jpg

格式效果

8558467.jpg

Pinia

安装

npm install pinia

添加 src/stores/index.ts

import { createPinia } from 'pinia'
// 创建
const pinia = createPinia()
// 导出
export default pinia

引入

// main.ts

import {createApp} from 'vue'
import App from './App.vue'
// ...
import pinia from "./stores/index"

const app = createApp(App)
// ...
app.use(pinia)
app.mount('#app')

使用pinia保存路由信息

新建 src/store/routesList.ts

import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const routesList = defineStore('routesList', {
  state: () => ({
    routesList: [],
  }),
  actions: {
    // 设置路由集合
    async setRouterList(data: any) {
      this.routesList = data
    },
  },
})

然后再前置路由守卫中调用方法保存

import { routesList } from '../stores/routesList'

// 路由加载前
router.beforeEach(async (to, from, next) => {
  const routerList = routesList()
  await routerList.setRouterList(routes[0].children)
  next()
})

递归显示多级菜单

新建 src/layout/menu/Menu.vue

<template>
  <el-menu default-active="2" class="el-menu-vertical-demo" @open="handleOpen" @close="handleClose">
    <!--遍历路由集合-->
    <template v-for="item in state.routerList" :key="item.path">
      <!--二级展开菜单-->
      <el-sub-menu :index="item.path" v-if="item.children && item.children.length > 0" :key="item.path">
        <template #title>
          <!--<SvgIcon :name="val.meta.icon" />-->
          <span>{{ item.name }}</span>
        </template>
        <sub-menu :chil="item.children" />
      </el-sub-menu>
      <!--一级路由-->
      <template v-else>
        <el-menu-item :index="item.path" :key="item.path">
          <!--<SvgIcon :name="item.meta.icon" />-->
          <span>{{ item.name }}</span>
        </el-menu-item>
      </template>
    </template>
  </el-menu>
</template>

<script lang="ts" setup>
import { useRouter, useRoute } from 'vue-router'
import { Document, Menu as IconMenu, Location, Setting } from '@element-plus/icons-vue'
import { onMounted, reactive, ref } from 'vue'
import { routesList } from '../../stores/routesList'
import SubMenu from './SubMenu.vue'

const state = reactive({
  router: useRouter(),
  routerList: routesList().routesList,
})
const handleOpen = (key: string, keyPath: string[]) => {
  // console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
  // console.log(key, keyPath)
}
</script>

<style>
.el-menu {
  border-right: 0;
  width: 200px;
}
</style>

新建 src/layout/menu/SubMenu.vue

<template>
  <template v-for="val in props.chil">
    <el-sub-menu :index="val.path" :key="val.path" v-if="val.children && val.children.length > 0">
      <template #title>
        <span>{{ val.name }}</span>
      </template>
      <!--组件递归-->
      <sub-menu :chil="val.children" />
    </el-sub-menu>
    <template v-else>
      <el-menu-item :index="val.path" :key="val.path">
        <span>{{ val.name }}</span>
      </el-menu-item>
    </template>
  </template>
</template>

<script lang="ts" setup>
import { defineProps } from 'vue'

const props = defineProps(['chil'])
</script>

效果展示

8558467.jpg

根据权限显示菜单

首先设置两个角色:admin、common,分别表示管理员和普通用户。

创建 src/stores/userInfo.ts,暂时写死一个用户数据

import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const userInfo = defineStore('userInfo', {
  state: () => ({
    // 用户名称
    userName: 'admin',
    // 用户id
    userId: 'zx-001',
    // 用户权限 admin:管理员,common:普通用户
    roles: ['common'],
    // 用户头像
    portrait:
      'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500',
  }),
  actions: {
    // 设置用户信息
    setUserInfo(info: any) {},
  },
})

创建 src/stores/routesList.ts 文件,存放路由集合信息

import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const routesList = defineStore('routesList', {
  state: () => ({
    routesList: [],
  }),
  actions: {
    // 设置路由集合
    async setRouterList(data: any) {
      this.routesList = data
    },
  },
})

创建 src/router/routes.ts 菜单数据文件,其中 meta 里面有 roles 数组,表示只要用户的角色在这里就显示当前菜单

// src/router/routes.ts
import Layout from '../layout/index.vue'
import Parent from '../layout/routerview/Parent.vue'

/**
 * meta 属性意义
 *  roles 设置那些权限可见。admin:管理员,common:普通职工
 */

export default [
  {
    path: '/',
    name: 'router.home',
    component: Layout,
    redirect: '/home',
    children: [
      {
        path: '/home',
        meta: {
          roles: ['admin', 'common'],
        },
        name: 'router.home',
        component: () => import('../view/home/home.vue'),
      },
      {
        path: '/about',
        meta: {
          roles: ['admin', 'common'],
        },
        name: 'router.about',
        component: () => import('../view/about/about.vue'),
      },
      {
        path: '/order',
        meta: {
          roles: ['admin', 'common'],
        },
        name: 'router.order',
        component: Parent,
        children: [
          {
            path: 'list',
            meta: {
              roles: ['admin', 'common'],
            },
            name: 'router.order_list',
            component: () => import('../view/order/list.vue'),
          },
          {
            path: 'stock',
            meta: {
              roles: ['admin', 'common'],
            },
            name: 'router.order_stock',
            component: Parent,
            children: [
              {
                path: 'price',
                meta: {
                  roles: ['admin', 'common'],
                },
                name: 'router.order_stock_price',
                component: () => import('../view/order/price.vue'),
              },
            ],
          },
        ],
      },
      {
        path: '/system',
        name: 'router.system',
        meta: {
          roles: ['admin'],
        },
        component: Parent,
        children: [
          {
            path: 'menu',
            meta: {
              roles: ['admin'],
            },
            name: 'router.system_menu',
            component: () => import('../view/system/menu.vue'),
          },
          {
            path: 'role',
            meta: {
              roles: ['admin'],
            },
            name: 'router.system_role',
            component: () => import('../view/system/role.vue'),
          },
          {
            path: 'user',
            meta: {
              roles: ['admin'],
            },
            name: 'router.system_user',
            component: () => import('../view/system/user.vue'),
          },
          {
            path: 'dept',
            meta: {
              roles: ['admin'],
            },
            name: 'router.system_dept',
            component: () => import('../view/system/dept.vue'),
          },
        ],
      },
    ],
  },
]

修改 src/router/index.ts 文件如下

// 1.从vue-router导出两个方法使用
import { createRouter, createWebHashHistory } from 'vue-router'
import { routesList } from '../stores/routesList'
import routes from './routes'

// 3. 创建路由实例并传递 `routes` 配置
// 你可以在这里输入更多的配置,但我们在这里
// 暂时保持简单
const router = createRouter({
  // 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
  history: createWebHashHistory(),
  routes, // `routes: routes` 的缩写
})

// 路由加载前
router.beforeEach(async (to, from, next) => {
  const routerList = routesList()
  await routerList.setRouterList(routes[0].children)
  next()
})

// 导出路由
export default router

添加 src/router/filterRouter.ts 文件,待会会用到这里面的方法

/**
 * 判断路由 `meta.roles` 中是否包含当前登录用户权限字段
 * @param roles 用户权限标识,在 userInfos(用户信息)的 roles(登录页登录时缓存到浏览器)数组
 * @param route 当前循环时的路由项
 * @returns 返回对比后有权限的路由项
 */
export function hasRoles(roles: any, route: any) {
  if (route.meta && route.meta.roles)
    return roles.some((role: any) => route.meta.roles.includes(role))
  else return true
}

/**
 * 获取当前用户权限标识去比对路由表,设置递归过滤有权限的路由
 * @param routes 当前路由 children
 * @param roles 用户权限标识,在 userInfos(用户信息)的 roles(登录页登录时缓存到浏览器)数组
 * @returns 返回有权限的路由数组 `meta.roles` 中控制
 */
export function setFilterHasRolesMenu(routes: any, roles: any) {
  const menu: any = []
  routes.forEach((route: any) => {
    const item = { ...route }
    if (hasRoles(roles, item)) {
      if (item.children) {
        item.children = setFilterHasRolesMenu(item.children, roles)
      }
      menu.push(item)
    }
  })
  return menu
}

/**
 * 路由扁平化方法
 * @param routes
 */
export function flatten(routes: any) {
  return routes.reduce(
    (arr: any, old: any) => arr.concat([old], flatten(old.children || [])),
    []
  )
}

src/layout/menu/Menu.vue 组件中添加如下逻辑,首先进入页面触发 onBeforeMount,在该生命周期中调用 getRouterListByRole 方法获取菜单数据,getRouterListByRole 方法中又调用 setFilterHasRolesMenu 方法,来实现根据权限获取不同菜单

import { onBeforeRouteUpdate, useRoute } from 'vue-router'
import { onBeforeMount, reactive } from 'vue'
import { routesList } from '../../stores/routesList'
import { userInfo } from '../../stores/userInfo'
import SubMenu from './SubMenu.vue'
import { Menu as IconMenu } from '@element-plus/icons-vue'
import { setFilterHasRolesMenu } from '../../router/filterRouter'
import { useI18n } from 'vue-i18n'

const { t } = useI18n()

const state = reactive({
  routerList: [], // 路由数据
  active: useRoute().path, // 根据路由默认选中菜单
})

onBeforeMount(() => {
  // 获取当前的组件信息
  let routerList = getRouterListByRole()
  routerList.forEach((item: any) => {
    if (item.children) {
      resolvePath(item.path, item.children)
    }
  })
  // 赋值
  state.routerList = routerList
})
// 递归遍历深层菜单的路径
const resolvePath = (parentPath: string, children: Array<any>) => {
  children.forEach((item: any) => {
    item.path = parentPath + '/' + item.path
    if (item.children) {
      resolvePath(item.path, item.children)
    }
  })
}
// 路由更新时更新菜单选中
onBeforeRouteUpdate((to) => {
  state.active = to.path
})
// 根据用户权限获取菜单数据
const getRouterListByRole = () => {
  const roles = userInfo().roles
  const routerList = JSON.parse(JSON.stringify(routesList().routesList))
  return setFilterHasRolesMenu(routerList, roles)
}

效果显示

普通用户没有系统管理菜单

8558467.jpg

管理员可以看到系统管理

8558467.jpg

实现点击菜单进行路由跳转

首先添加下面代码,作用是可以将内层的菜单设置为全路径

onBeforeMount(() => {
  // 获取当前的组件信息
  let routerList = JSON.parse(JSON.stringify(routesList().routesList))
  routerList.forEach((item: any) => {
    if (item.children) {
      resolvePath(item.path, item.children)
    }
  })
  // 赋值
  state.routerList = routerList
})
// 递归遍历深层菜单的路径
const resolvePath = (parentPath: string, children: Array<any>) => {
  children.forEach((item: any) => {
    item.path = parentPath + '/' + item.path
    if (item.children) {
      resolvePath(item.path, item.children)
    }
  })
}

然后开启 Menu 组件的 router 模式即可

动态面包屑导航

完整代码

<template>
  <el-breadcrumb separator="/">
    <el-breadcrumb-item
      v-for="(item, index) in state.breadcrumbList"
      :key="index"
    >
      <span class="breadcrumb-text">{{ item.name }}</span>
    </el-breadcrumb-item>
  </el-breadcrumb>
</template>

<script lang="ts" setup>
import { watch, ref, reactive } from 'vue'
// 引入路由
import { useRoute } from 'vue-router'

const state = reactive({
  breadcrumbList: [],
  route: useRoute(),
})
// 初始化面包屑
const initBreadcrumbList = () => {
  // route.matched 可以获取当前路由的完整路由表
  state.breadcrumbList = state.route.matched.slice(1)
}
// 监听路由变化
watch(
  state.route,
  () => {
    initBreadcrumbList()
  },
  { deep: true, immediate: true }
)
</script>

效果

8558467.jpg

页面最大化和最小化

<template>
  <div class="full">
    <el-icon :size="20" @click="handleFullScreen" class="icon-color">
      <FullScreen />
    </el-icon>
  </div>
</template>

<script setup>
import { reactive, computed } from 'vue'

const state = reactive({
  fullscreen: false,
})
const handleFullScreen = () => {
  let element = document.documentElement
  // 判断是否已经是全屏
  // 如果是全屏,退出
  if (state.fullscreen) {
    if (document.exitFullscreen) {
      document.exitFullscreen()
    } else if (document.webkitCancelFullScreen) {
      document.webkitCancelFullScreen()
    } else if (document.mozCancelFullScreen) {
      document.mozCancelFullScreen()
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen()
    }
  } else {
    // 否则,进入全屏
    if (element.requestFullscreen) {
      element.requestFullscreen()
    } else if (element.webkitRequestFullScreen) {
      element.webkitRequestFullScreen()
    } else if (element.mozRequestFullScreen) {
      element.mozRequestFullScreen()
    } else if (element.msRequestFullscreen) {
      // IE11
      element.msRequestFullscreen()
    }
  }
  // 改变当前全屏状态
  state.fullscreen = !state.fullscreen
}
</script>

<style scoped>
.full {
  width: 35px;
  height: 35px;
  background: var(--el-color-primary-light-9);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.icon-color {
  color: var(--el-color-primary);
}
</style>

主题颜色自定义

添加一个初始化配置文件 src/stores/styleconfig.ts

import { defineStore } from 'pinia'

let state = {
  // 默认 primary 主题颜色
  primary: '#752bec',
  // 白色背景
  bgWhite: '#ffffff',
}

// 从缓存中读取预设的样式配置
const config = localStorage.getItem('styleConfig')
if (config) {
  state = JSON.parse(config)
}

// 第一个参数是应用程序中 store 的唯一 id
export const styleConfig = defineStore('styleConfig', {
  state: () => state,
})

新建一个工具文件 src/utils/theme.ts

import { ElMessage } from 'element-plus';

/**
 * hex颜色转rgb颜色
 * @param str 颜色值字符串
 * @returns 返回处理后的颜色值
 */
export function hexToRgb(str: any) {
   let hexs: any = '';
   let reg = /^\#?[0-9A-Fa-f]{6}$/;
   if (!reg.test(str)) return ElMessage.warning('输入错误的hex');
   str = str.replace('#', '');
   hexs = str.match(/../g);
   for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16);
   return hexs;
}

/**
 * rgb颜色转Hex颜色
 * @param r 代表红色
 * @param g 代表绿色
 * @param b 代表蓝色
 * @returns 返回处理后的颜色值
 */
export function rgbToHex(r: any, g: any, b: any) {
   let reg = /^\d{1,3}$/;
   if (!reg.test(r) || !reg.test(g) || !reg.test(b)) return ElMessage.warning('输入错误的rgb颜色值');
   let hexs = [r.toString(16), g.toString(16), b.toString(16)];
   for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`;
   return `#${hexs.join('')}`;
}

/**
 * 加深颜色值
 * @param color 颜色值字符串
 * @param level 加深的程度,限0-1之间
 * @returns 返回处理后的颜色值
 */
export function getDarkColor(color: string, level: number) {
   let reg = /^\#?[0-9A-Fa-f]{6}$/;
   if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值');
   let rgb = hexToRgb(color);
   for (let i = 0; i < 3; i++) rgb[i] = Math.floor(rgb[i] * (1 - level));
   return rgbToHex(rgb[0], rgb[1], rgb[2]);
}

/**
 * 变浅颜色值
 * @param color 颜色值字符串
 * @param level 加深的程度,限0-1之间
 * @returns 返回处理后的颜色值
 */
export function getLightColor(color: string, level: number) {
   let reg = /^\#?[0-9A-Fa-f]{6}$/;
   if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值');
   let rgb = hexToRgb(color);
   for (let i = 0; i < 3; i++) rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i]);
   return rgbToHex(rgb[0], rgb[1], rgb[2]);
}

然后再新建一个配置文件 src/config/styleSetting.ts

import { styleConfig } from '../stores/styleconfig'
import { getLightColor } from '../utils/theme'

//设置主题颜色
export const setPrimaryColor = (color = '') => {
  const el = document.documentElement
  // 设置主题颜色变量
  el.style.setProperty('--el-color-primary', color || styleConfig().primary)
  // 颜色变浅
  for (let i = 1; i <= 9; i++) {
    el.style.setProperty(
      `--el-color-primary-light-${i}`,
      `${getLightColor(color || styleConfig().primary, i / 10)}`
    )
  }
}

// 设置主背景颜色
export const setBgWhite = (color = '') => {
  const el = document.documentElement
  el.style.setProperty('--el-color-bg-white', color || styleConfig().bgWhite)
}

// 页面加载时默认执行所有方法
const setStyle = () => {
  setPrimaryColor()
  setBgWhite()
}

export default setStyle

然后在 main.ts 中引入 styleSetting.ts

import setStyle from "./config/styleSetting";

app.use(setStyle)

然后在 css 中需要设置主颜色时,直接使用变量来代替颜色值,例如下面是设置菜单选中时的颜色

>>> .el-menu-item.is-active {
  color: var(--el-color-primary);
  background: var(--el-color-primary-light-9);
  transition: 0.5s;
}

>>> .el-menu-item.is-active:after {
  content: '';
  width: 5px;
  height: 100%;
  background-color: var(--el-color-primary);
  position: absolute;
  left: 0;
  transition: 0.5s;
}

默认显示的主题颜色

8558467.jpg

然后写一个设置主题颜色的方法,来实时的更新 --el-color-primary 值,点击保存后把配置保存在缓存中

<template>
  <el-icon :size="20" class="icon-color" @click="state.isShow = true">
    <Brush />
  </el-icon>
  <el-drawer
    v-model="state.isShow"
    title="主题设置"
    direction="rtl"
    size="380px"
    :before-close="beforeClose"
  >
    <template #default>
      <el-form
        :model="state.config"
        label-width="100px"
        class="content"
        label-position="left"
      >
        <el-form-item label="主题颜色">
          <el-color-picker
            v-model="state.config.primary"
            @change="changePrimary"
          />
        </el-form-item>
      </el-form>
    </template>
    <template #footer>
      <div style="flex: auto">
        <el-button @click="state.isShow = false">关闭</el-button>
        <el-button type="primary" @click="confirmClick">保存</el-button>
      </div>
    </template>
  </el-drawer>
</template>

<script lang="ts" setup>
import { reactive } from 'vue'
import { setPrimaryColor } from '../../config/styleSetting'
import { styleConfig } from '../../stores/styleconfig'

const state = reactive({
  // 是否显示右侧设置框
  isShow: false,
  // 主题配置对象
  config: {
    // 默认 primary 主题颜色
    primary: styleConfig().primary,
    // 白色背景
    bgWhite: styleConfig().bgWhite,
  },
})
// 关闭设置框
const beforeClose = () => {
  state.isShow = false
}
//修改主题色
const changePrimary = (color: string) => {
  setPrimaryColor(color)
}
// 保存配置
const confirmClick = () => {
  localStorage.setItem('styleConfig', JSON.stringify(state.config))
  window.location.reload()
}
</script>

<style scoped>
.icon-color {
  width: 35px;
  height: 35px;
  background: var(--el-color-primary-light-9);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--el-color-primary);
}

.content {
  border-top: 1px solid var(--el-color-primary-light-6);
  padding-top: 15px;
}
</style>

修改一个颜色后,页面整体颜色都会发生变化

8558467.jpg

设置国际化

引入指定版本的 "vue-i18n": "^9.1.10" ,否则高版本会报错

npm install [email protected]

新建文件夹 src/i18n,这个文件夹下新建如下文件:

  • lang
    • en.ts
    • zh.ts
  • router
    • en.ts
    • zh.ts
  • index.ts

内容分别如下

// pages/en.ts
export default {
    login: {
        login: 'login',
        userName: 'userName',
        password: 'password'
    }
}
// pages/zh.ts
export default {
    login: {
        login: '登录',
        userName: '用户名',
        password: '密码'
    }
}
// router/en.ts
export default {
  router: {
    title: 'ZX-SYSTEM',
    home: 'home',
    about: 'about',
    order: 'mall management',
    order_list: 'orderList',
    order_stock: 'inventory',
    order_stock_price: 'price',
    system: 'system management',
    system_menu: 'menu',
    system_role: 'role',
    system_user: 'user',
    system_dept: 'department',
  },
}
// router/en.ts
export default {
    router: {
        title: 'ZX-管理系统',
        home: "首页",
        about: "关于我",
        order: "商城管理",
        order_list: "订单列表",
        order_stock: "库存管理",
        order_stock_price: "价格管理",
        system: "系统管理",
        system_menu: "菜单管理",
        system_role: "角色管理",
        system_user: "用户管理",
        system_dept: "部门管理",
    }
}

然后在 index.ts 里面整合

// index.ts
import {createI18n} from 'vue-i18n'
import pagesEn from "./pages/en"
import pagesZh from "./pages/zh"

import layoutEn from "./router/en"
import layoutZh from "./router/zh"


/**
 * ./pages 表示各个页面的国际化
 * ./router 表示左侧菜单的国际化
 */

const messages = {
    en: {
        ...pagesEn,
        ...layoutEn
    },
    zh: {
        ...pagesZh,
        ...layoutZh
    },
}
const language = (navigator.language || 'en').toLocaleLowerCase() // 这是获取浏览器的语言
const i18n = createI18n({
    legacy: false,
    locale: localStorage.getItem('lang') || language.split('-')[0] || 'en', // 首先从缓存里拿,没有的话就用浏览器语言,
    fallbackLocale: 'en', // 设置备用语言
    messages,
})

export default i18n

main.ts 中引入

import i18n from "./i18n/index"

const app = createApp(App)
app.use(i18n)
app.mount('#app')

使用也非常简单,根据前缀不同,会自动的显示不同的语言

在页面中使用

<template>
  <div>
    <div>{{ t('login.userName') }}</div>
  </div>
</template>

<script lang="ts" setup>
import {useI18n} from 'vue-i18n'

const {t} = useI18n()
</script>

在菜单中使用,修改 name 的值,不能写死为固定的中文名,而是改成国际化文件对应的属性

{
  path: 'stock',
  meta: {
    roles: ['admin', 'common'],
  },
  name: 'router.order_stock',
  component: Parent,
  children: [
    {
      path: 'price',
      meta: {
        roles: ['admin', 'common'],
      },
      name: 'router.order_stock_price',
      component: () => import('../view/order/price.vue'),
    },
  ],
},

然后在组件中使用 t 转义

<template v-else>
  <el-menu-item :index="item.path" :key="item.path">
    <el-icon>
      <icon-menu />
    </el-icon>
    <span>{{ t(item.name) }}</span>
  </el-menu-item>
</template>
import { useI18n } from 'vue-i18n'

const { t } = useI18n()

然后可以添加一个方法,来切换中英文显示

<template>
  <div class="full" @click="taggerLang">
    <div class="icon-color">
      {{ state.lang === 'zh' ? '中' : 'en' }}
    </div>
  </div>
</template>

<script setup>
import {reactive, computed, onMounted} from 'vue'

const state = reactive({
  lang: 'zh',
})
onMounted(() => {
  const lang = localStorage.getItem("lang")
  if (!lang || lang === 'zh') {
    state.lang = 'zh'
  } else {
    state.lang = 'en'
  }
})

const taggerLang = () => {
  if (state.lang === 'zh') {
    state.lang = 'en'
  } else {
    state.lang = 'zh'
  }
  localStorage.setItem("lang",state.lang)
  window.location.reload()
}
</script>

英文界面

8558467.jpg

中文界面

8558467.jpg

动态路由

添加返回模拟数据的方法

模拟实现从后端直接获取路由进行菜单展示

首先新建:src/api/testrouter/index.ts,这个文件用来模拟后端接口返回数据

// 管理员看到的菜单
export const adminRouter = () => {
  return {
    code: 200,
    msg: '成功',
    data: {
      rows: [
        {
          path: '/home',
          meta: {
            roles: ['admin', 'common'],
            title: 'router.home',
          },
          name: 'router.home',
          component: 'view/home/home.vue',
        },
        {
          path: '/about',
          meta: {
            roles: ['admin', 'common'],
            title: 'router.about',
          },
          name: 'router.about',
          component: 'view/about/about',
        },
        {
          path: '/order',
          meta: {
            roles: ['admin', 'common'],
            title: 'router.order',
          },
          name: 'router.order',
          component: 'layout/routerview/Parent',
          children: [
            {
              path: '/order/list',
              meta: {
                roles: ['admin', 'common'],
                title: 'router.order_list',
              },
              name: 'router.order_list',
              component: 'view/order/list',
            },
            {
              path: '/order/stock',
              meta: {
                roles: ['admin', 'common'],
                title: 'router.order_stock',
              },
              name: 'router.order_stock',
              component: 'layout/routerview/Parent',
              children: [
                {
                  path: '/order/price',
                  meta: {
                    roles: ['admin', 'common'],
                    title: 'router.order_stock_price',
                  },
                  name: 'router.order_stock_price',
                  component: 'view/order/price',
                },
              ],
            },
          ],
        },
        {
          path: '/system',
          name: 'router.system',
          meta: {
            roles: ['admin'],
            title: 'router.system',
          },
          component: 'layout/routerview/Parent',
          children: [
            {
              path: '/system/menu',
              meta: {
                roles: ['admin'],
                title: 'router.system_menu',
              },
              name: 'router.system_menu',
              component: 'view/system/menu',
            },
            {
              path: '/system/role',
              meta: {
                roles: ['admin'],
                title: 'router.system_role',
              },
              name: 'router.system_role',
              component: 'view/system/role',
            },
            {
              path: '/system/user',
              meta: {
                roles: ['admin'],
                title: 'router.system_user',
              },
              name: 'router.system_user',
              component: 'view/system/user',
            },
            {
              path: '/system/dept',
              meta: {
                roles: ['admin'],
                title: 'router.system_dept',
              },
              name: 'router.system_dept',
              component: 'view/system/dept',
            },
          ],
        },
      ],
    },
  }
}

// 普通用户看到的菜单
export const commonRouter = () => {
  return {
    code: 200,
    msg: '成功',
    data: {
      rows: [
        {
          path: '/home',
          meta: {
            roles: ['admin', 'common'],
            title: 'router.home',
          },
          name: 'router.home',
          component: 'view/home/home.vue',
        },
        {
          path: '/about',
          meta: {
            roles: ['admin', 'common'],
            title: 'router.about',
          },
          name: 'router.about',
          component: 'view/about/about',
        },
        {
          path: '/order',
          meta: {
            roles: ['admin', 'common'],
            title: 'router.order',
          },
          name: 'router.order',
          component: 'layout/routerview/Parent',
          children: [
            {
              path: '/order/list',
              meta: {
                roles: ['admin', 'common'],
                title: 'router.order_list',
              },
              name: 'router.order_list',
              component: 'view/order/list',
            },
            {
              path: '/order/stock',
              meta: {
                roles: ['admin', 'common'],
                title: 'router.order_stock',
              },
              name: 'router.order_stock',
              component: 'layout/routerview/Parent',
              children: [
                {
                  path: '/order/price',
                  meta: {
                    roles: ['admin', 'common'],
                    title: 'router.order_stock_price',
                  },
                  name: 'router.order_stock_price',
                  component: 'view/order/price',
                },
              ],
            },
          ],
        },
      ],
    },
  }
}

添加模拟获取菜单数据的接口

新建:src/api/menu/index.ts,这个里面来获取上面接口的返回值

import { adminRouter, commonRouter } from '../testrouter'
import { userInfo } from '../../stores/userInfo'

/**
 * 模拟获取后端返回的路由集合
 * @returns {Promise<unknown>}
 */
export const getRouterListFun = () => {
  const username = userInfo().getUserInfo().userName

  return new Promise((resolve, reject) => {
    if (username === 'admin') {
      resolve(adminRouter())
    } else {
      resolve(commonRouter())
    }
  })
}

添加模拟返回用户信息的接口

接着新建模拟登录接口:src/api/login/index.ts

export const getUerInfoFun = (parames: any) => {
  return new Promise((resolve, reject) => {
    // 获取登录表单传递过来的参数
    console.log(parames)
    resolve({
      code: 200,
      msg: '成功',
      data: {
        info: {
          userName: 'admin',
          photo:
            'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500',
          time: new Date().getTime(),
          roles: ['admin'],
          authBtnList: ['btn.add', 'btn.del', 'btn.edit', 'btn.link'],
          token: '123456',
        },
      },
    })
  })
}

封装获取菜单,保存菜单的方法

新建 src/router/backEnd.ts,用于处理后端返回的数据

import { RouteRecordRaw } from 'vue-router'
import { Session } from '../utils/storage'
import { getRouterListFun } from '../api/menu'
import { useRequestOldRoutes } from '../stores/requestOldRoutes'
import { dynamicRoutes, notFoundAndNoPower } from './routes'
import { formatFlatteningRoutes, formatTwoStageRoutes, router } from './index'
import { routesList } from '../stores/routesList'
import { useTagsViewRoutes } from '../stores/tagsViewRoutes'

const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}')
const viewsModules: any = import.meta.glob('../view/**/*.{vue,tsx}')

// 后端控制路由

/**
 * 获取目录下的 .vue、.tsx 全部文件
 * key是组件的地址,value为 component 函数
 * @method import.meta.glob
 * @link 参考:https://cn.vitejs.dev/guide/features.html#json
 */
const dynamicViewsModules: Record<string, Function> = Object.assign(
  {},
  { ...layouModules },
  { ...viewsModules }
)

/**
 * 后端控制路由:初始化方法,防止刷新时路由丢失
 * @method NextLoading 界面 loading 动画开始执行
 * @method useUserInfo().setUserInfos() 触发初始化用户信息 pinia
 * @method useRequestOldRoutes().setRequestOldRoutes() 存储接口原始路由(未处理component),根据需求选择使用
 * @method setAddRoute 添加动态路由
 * @method setFilterMenuAndCacheTagsViewRoutes 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
 */
export async function initBackEndControlRoutes() {
  // 无 token 停止执行下一步
  if (!Session.get('token')) return false
  // 获取路由菜单数据
  const res: any = await getRouterListFun()
  // 存储接口原始路由(未处理component),根据需求选择使用
  await useRequestOldRoutes().setRequestOldRoutes(
    JSON.parse(JSON.stringify(res.data.rows))
  )
  // 清空路由,避免出错
  dynamicRoutes[0].children = []
  // 处理路由(component),替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由
  dynamicRoutes[0].children = await backEndComponent(res.data.rows)
  // 添加动态路由
  await setAddRoute()
  // 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
  await setFilterMenuAndCacheTagsViewRoutes()
}

/**
 * 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
 * @description 用于左侧菜单、横向菜单的显示
 * @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide)
 */
export function setFilterMenuAndCacheTagsViewRoutes() {
  // 保存处理后的数据
  routesList().setRouterList(dynamicRoutes[0].children)
  setCacheTagsViewRoutes()
}

/**
 * 缓存多级嵌套数组处理后的一维数组
 * @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide)
 */
export function setCacheTagsViewRoutes() {
  const storesTagsView = useTagsViewRoutes()
  storesTagsView.setTagsViewRoutes(
    formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes))[0].children
  )
}

/**
 * 处理路由格式及添加捕获所有路由或 404 Not found 路由
 * @description 替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由
 * @returns 返回替换后的路由数组
 */
export function setFilterRouteEnd() {
  let filterRouteEnd: any = formatTwoStageRoutes(
    formatFlatteningRoutes(dynamicRoutes)
  )
  filterRouteEnd[0].children = [
    ...filterRouteEnd[0].children,
    ...notFoundAndNoPower,
  ]
  return filterRouteEnd
}

/**
 * 添加动态路由
 * @method router.addRoute
 * @description 此处循环为 dynamicRoutes(/@/router/route)第一个顶级 children 的路由一维数组,非多级嵌套
 * @link 参考:https://next.router.vuejs.org/zh/api/#addroute
 */
export async function setAddRoute() {
  await setFilterRouteEnd().forEach((route: RouteRecordRaw) => {
    router.addRoute(route)
  })
}

/**
 * 后端路由 component 转换
 * @param routes 后端返回的路由表数组
 * @returns 返回处理成函数后的 component
 */
export function backEndComponent(routes: any) {
  if (!routes) return
  return routes.map((item: any) => {
    if (item.component)
      item.component = dynamicImport(
        dynamicViewsModules,
        item.component as string
      )
    item.children && backEndComponent(item.children)
    return item
  })
}

/**
 * 后端路由 component 转换函数
 * @param dynamicViewsModules 获取目录下的 .vue、.tsx 全部文件
 * @param component 当前要处理项 component
 * @returns 返回处理成函数后的 component
 */
export function dynamicImport(
  dynamicViewsModules: Record<string, Function>,
  component: string
) {
  const keys = Object.keys(dynamicViewsModules)
  const matchKeys = keys.filter((key) => {
    const k = key.replace(/..\//, '')
    return k.startsWith(`${component}`) || k.startsWith(`/${component}`)
  })
  if (matchKeys?.length === 1) {
    const matchKey = matchKeys[0]
    return dynamicViewsModules[matchKey]
  }
  if (matchKeys?.length > 1) {
    return false
  }
}

添加辅助文件

上面的代码中引用了下面的文件

src/stores/requestOldRoutes.ts

import { defineStore } from 'pinia'

/**
 * 后端返回原始路由(未处理时)
 * @methods setCacheKeepAlive 设置接口原始路由数据
 */
export const useRequestOldRoutes = defineStore('useRequestOldRoutes', {
  state: () => ({
    requestOldRoutes: [],
  }),
  actions: {
    async setRequestOldRoutes(routes: any) {
      this.requestOldRoutes = routes
    },
  },
})

src/stores/routesList.ts

import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const routesList = defineStore('routesList', {
  state: () => ({
    routesList: [],
  }),
  actions: {
    // 设置路由集合
    setRouterList(data: any) {
      this.routesList = data
    },
  },
})

src/stores/tagsViewRoutes.ts

import { defineStore } from 'pinia'
import { Session } from '../utils/storage'

/**
 * TagsView 路由列表
 * @methods setTagsViewRoutes 设置 TagsView 路由列表
 * @methods setCurrenFullscreen 设置开启/关闭全屏时的 boolean 状态
 */
export const useTagsViewRoutes = defineStore('tagsViewRoutes', {
  state: (): any => ({
    tagsViewRoutes: [],
    isTagsViewCurrenFull: false,
  }),
  actions: {
    async setTagsViewRoutes(data: Array<string>) {
      this.tagsViewRoutes = data
    },
    setCurrenFullscreen(bool: Boolean) {
      Session.set('isTagsViewCurrenFull', bool)
      this.isTagsViewCurrenFull = bool
    },
  },
})

修改导出路由的文件

然后改写 src/router/routes.ts,分成三个部分导出

import Layout from '../layout/index.vue'

// 动态路由
export const dynamicRoutes = [
  {
    path: '/',
    name: '/',
    component: Layout,
    redirect: '/home',
    meta: {
      isKeepAlive: true,
      title: '首页',
    },
    children: [],
  },
]

// 定义404,401等路由
export const notFoundAndNoPower = [
  {
    path: '/:path(.*)*',
    name: 'notFound',
    component: () => import('@/view/error/404.vue'),
    meta: {
      title: '404',
      isHide: true,
    },
  },
  {
    path: '/401',
    name: 'noPower',
    component: () => import('@/view/error/401.vue'),
    meta: {
      title: '404',
      isHide: true,
    },
  },
]

/**
 * 定义静态路由(默认路由)
 */
export const staticRoutes = [
  {
    path: '/login',
    name: 'router.login',
    component: () => import('../view/login/index.vue'),
    meta: {
      title: 'router.login',
    },
  },
]

修改router文件

修改 src/router/index.ts,默认只加载一个 staticRoutes

// 1.从vue-router导出两个方法使用
import { createRouter, createWebHashHistory, useRouter } from 'vue-router'
import { routesList } from '../stores/routesList'
import { staticRoutes } from './routes'
import { Session } from '../utils/storage'
import { initBackEndControlRoutes } from './backEnd'

export const router = createRouter({
  // 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
  history: createWebHashHistory(),
  routes: staticRoutes, // 加载静态路由
})

/**
 * 路由多级嵌套数组处理成一维数组
 * @param arr 传入路由菜单数据数组
 * @returns 返回处理后的一维路由菜单数组
 */
export function formatFlatteningRoutes(arr: any) {
  if (arr.length <= 0) return false
  for (let i = 0; i < arr.length; i++) {
    if (arr[i].children) {
      arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1))
    }
  }
  return arr
}

/**
 * 一维数组处理成多级嵌套数组(只保留二级:也就是二级以上全部处理成只有二级,keep-alive 支持二级缓存)
 * @description isKeepAlive 处理 `name` 值,进行缓存。顶级关闭,全部不缓存
 * @link 参考:https://v3.cn.vuejs.org/api/built-in-components.html#keep-alive
 * @param arr 处理后的一维路由菜单数组
 * @returns 返回将一维数组重新处理成 `定义动态路由(dynamicRoutes)` 的格式
 */
export function formatTwoStageRoutes(arr: any) {
  if (arr.length <= 0) return false
  const newArr: any = []
  const cacheList: Array<string> = []
  arr.forEach((v: any) => {
    if (v.path === '/') {
      newArr.push({
        component: v.component,
        name: v.name,
        path: v.path,
        redirect: v.redirect,
        meta: v.meta,
        children: [],
      })
    } else {
      // 判断是否是动态路由(xx/:id/:name),用于 tagsView 等中使用
      if (v.path.indexOf('/:') > -1) {
        v.meta['isDynamic'] = true
        v.meta['isDynamicPath'] = v.path
      }
      newArr[0].children.push({ ...v })
    }
  })
  return newArr
}

// 路由加载前
router.beforeEach(async (to, from, next) => {
  const token = Session.get('token')
  if (to.path === '/login' && !token) {
    next()
  } else {
    if (!token) {
      next(
        `/login?redirect=${to.path}&params=${JSON.stringify(
          to.query ? to.query : to.params
        )}`
      )
      Session.clear()
    } else if (token && to.path === '/login') {
      next('/home')
    } else {
      // 判断pinia中是否有路由信息
      if (routesList().routesList.length === 0) {
        // 后端控制路由:路由数据初始化,防止刷新时丢失
        await initBackEndControlRoutes()
        // 动态添加路由:防止非首页刷新时跳转回首页的问题
        next({ ...to, replace: true })
      } else {
        next()
      }
    }
  }
})

// 导出路由
export default router

添加登录页面完成测试

最后添加登录页面

<template>
  <div>
    <el-button type="primary" @click="login">登录</el-button>
  </div>
</template>

<script lang="ts" setup>
import { reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getUerInfoFun } from '../../api/login'
import { userInfo } from '../../stores/userInfo'
import { initBackEndControlRoutes } from '../../router/backEnd'

const state = reactive({
  router: useRouter(),
  route: useRoute(),
  loginForm: {
    username: 'admin',
    password: '123456',
    code: 1234,
  },
})

const login = () => {
  // 获取用户信息
  getUerInfoFun(state.loginForm).then(async (res: any) => {
    // 保存用户基本信息
    await userInfo().setUserInfos(res.data.info)
    // 获取路由信息
    await initBackEndControlRoutes()
    // 进行路由跳转
    siginSuccess()
  })
}

const siginSuccess = () => {
  // 跳转到上次关闭的页面
  if (state.route.query?.redirect) {
    state.router.push({
      path: <string>state.route.query?.redirect,
      query:
        Object.keys(<string>state.route.query?.params).length > 0
          ? JSON.parse(<string>state.route.query?.params)
          : '',
    })
  } else {
    // 跳转到首页
    state.router.push('/')
  }
}
</script>

实现退出登录方法

<template>
  <span>{{ userInfo().getUserInfo().userName }}</span>
  <el-dropdown>
    <div class="user-img">
      <img :src="userInfo().getUserInfo().photo" />
    </div>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item @click="logOut"
          >{{ t('router.log_out') }}
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<script setup>
import { Session } from '@/utils/storage'
import { userInfo } from '@/stores/userInfo'
import { useI18n } from 'vue-i18n'
import { reactive } from 'vue'
import { useRouter } from 'vue-router'

const { t } = useI18n()

const state = reactive({
  router: useRouter(),
})

const logOut = () => {
  Session.clear()
  state.router.push({
    path: '/login',
  })
}
</script>

<style scoped lang="scss">
.user-img {
  width: 44px;
  height: 44px;
  border-radius: 50px;
  border: 2px var(--el-color-primary) solid;
  display: flex;
  align-items: center;
  justify-content: center;

  img {
    width: 100%;
    height: 100%;
    border-radius: 50px;
  }
}
</style>

测试不同人员返回不同菜单

首先我们直接写死一个用户名为 test 的用户来登录

8558467.jpg

查看菜单,没有系统管理的菜单

8558467.jpg

然后再用 admin 登录

8558467.jpg

修改后,重新登录查看菜单

8558467.jpg

在js中使用scss变量

首先创建 scss 变量文件 primary.module.scss

$primary-color: var(--el-color-primary);

:export {
  primaryColor: $primary-color
}

需要注意的是,在 vite 创建的项目中,如果你想在 js 里引用 scss 文件,需要在后缀前加上 .module

然后再 js 中引入,html 中直接使用对应的变量即可

<el-table
          :header-cell-style="{ background: exCss.primaryColor, color: '#fff' }"
          :data="state.tableData"
          stripe
          style="width: 100%"
>
    <el-table-column prop="date" label="Date" width="180" />
    <el-table-column prop="name" label="Name" width="180" />
    <el-table-column prop="address" label="Address" />
</el-table>
import exCss from '@/style/module/primary.module.scss'

我这里做了一个表头的背景色跟着主题色变化的功能

8558467.jpg

8558467.jpg

webstorm设置代码块

8558467.jpg

8558467.jpg

自动导入vue3相关Api

安装

npm install -D unplugin-vue-components unplugin-auto-import

修改配置

// vite.config.ts

import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {ElementPlusResolver} from 'unplugin-vue-components/resolvers'


// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        vue(),
        AutoImport({
            // Auto import functions from Vue, e.g. ref, reactive, toRef...
            // 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
            imports: ['vue'],


            // Auto import functions from Element Plus, e.g. ElMessage, ElMessageBox... (with style)
            // 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
            resolvers: [
                ElementPlusResolver(),
            ],
        }),

        Components({
            resolvers: [
                // 自动导入 Element Plus 组件
                ElementPlusResolver(),
            ],
        }),
    ]
})

添加自动依赖

重启项目后会自动生成两个文件

8558467.jpg

其中 auto-import.d.ts 文件里面声明了所有可以自动引入的 Api

使用

设置完成后,在页面使用 reactive,ref,onMounted 等函数时,无需从 vue 中导出,可以直接使用。示例如下

在使用过程中也通过 webStorm 可以看到改函数的来源

8558467.jpg

<template>
  <div>
    count:{{ state.total }}

    <el-button @click="add">添加</el-button>
  </div>
</template>
<script setup>

const state = reactive({
  total: 0
})

const add = () => {
  state.total += 1
}
</script>

效果在页面中可以正常显示,控制台也没有报错

8558467.jpg

自定义全局loading

首先添加css样式文件 src/style/loading.scss

.loading-next {
  position: absolute;
  display: flex;
  width: 100vw;
  height: 100vh;
  z-index: 99999;

  &::before {
    content: "";
    width: 100%;
    height: 100%;
    background-color: black;
    opacity: 0.6;
    z-index: -1;
  }
}

.loading-next .loading-next-box {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.loading-next .loading-next-box-warp {
  width: 80px;
  height: 80px;
}

.loading-next .loading-next-box-warp .loading-next-box-item {
  width: 33.333333%;
  height: 33.333333%;
  background: var(--el-color-primary);
  float: left;
  animation: loading-next-animation 1.2s infinite ease;
  border-radius: 1px;
}

.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(7) {
  animation-delay: 0s;
}

.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(4),
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(8) {
  animation-delay: 0.1s;
}

.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(1),
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(5),
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(9) {
  animation-delay: 0.2s;
}

.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(2),
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(6) {
  animation-delay: 0.3s;
}

.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(3) {
  animation-delay: 0.4s;
}

@keyframes loading-next-animation {
  0%,
  70%,
  100% {
    transform: scale3D(1, 1, 1);
  }
  35% {
    transform: scale3D(0, 0, 1);
  }
}

添加 src/utils/loading.ts

import { nextTick } from 'vue'
import '../style/loading.scss'

/**
 * 页面全局 Loading
 * @method start 创建 loading
 * @method done 移除 loading
 */
export const zxLoading = {
  // 创建 loading
  show: () => {
    const bodys: Element = document.body
    const div = <HTMLElement>document.createElement('div')
    div.setAttribute('class', 'loading-next')
    const htmls = `
         <div class="loading-next-box">
            <div class="loading-next-box-warp">
               <div class="loading-next-box-item"></div>
               <div class="loading-next-box-item"></div>
               <div class="loading-next-box-item"></div>
               <div class="loading-next-box-item"></div>
               <div class="loading-next-box-item"></div>
               <div class="loading-next-box-item"></div>
               <div class="loading-next-box-item"></div>
               <div class="loading-next-box-item"></div>
               <div class="loading-next-box-item"></div>
            </div>
         </div>
      `
    div.innerHTML = htmls
    bodys.insertBefore(div, bodys.childNodes[0])
  },
  // 移除 loading
  hidden: () => {
    nextTick(() => {
      const el = <HTMLElement>document.querySelector('.loading-next')
      el?.parentNode?.removeChild(el)
    })
  },
}

使用

import { zxLoading } from '@/utils/loading'

zxLoading.show()
  
setTimeout(()=>{
    zxLoading.hidden()
},2000)

CSS设置动画

<template>
  <div class="err-text">
    <div>404</div>
  </div>
</template>

<style scoped>
.err-text {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 70px;
  /*设置动画名称,多少时间内完成,infinite:无限循环播放*/
  animation: zoom 0.7s infinite;
  /*开启反向动画*/
  animation-direction: alternate;
}

@keyframes zoom {
  0% {
    font-size: 70px;
    color: rgba(242, 80, 80, 0.96);
  }
  100% {
    font-size: 120px;
    color: #8935ea;
  }
}
</style>

效果展示

8558467.jpg

;