vue3+TS+Vite2+Element Plus管理系统通用模板(2022最新)

技术栈:vue3+TypeScript+Vite2+Pinia+Element Plus+VueRouter

初始化项目

安装vue-ts模板,和vite,并设置项目名称GBT(General Background Template)。

// npm
npm init vite@latest GBT --template vue-ts

// yarn 
yarn create vite GBT --template vue-ts

启动项目:

初始化项目后安装项目依赖,之后执行npm run dev命令启动项目,浏览器打开 http://127.0.0.1:5173/ 就可以看到启动后的项目

npm install --registry=https://registry.npm.taobao.org
//yarn install

npm run dev

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yBekxOiN-1670077065046)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/74adf791eda64198a00c5a8bc769629e~tplv-k3u1fbpfcp-watermark.image?)]

安装Element-Plus

Element-Plus官方文档有两种引入方式,这里使用全局引入。

// 图标组件需要单独安装
npm install element-plus @element-plus/icons-vue sass --registry=https://registry.npm.taobao.org

1.全局注册组件

在main.ts中引入ElementPlus和样式文件,并通过use方法安装ElementPlus插件。

// main.ts
import ElementPlus from 'element-plus'
import 'element-plus/theme-chalk/index.css'

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

2.全局组件类型声明&路径别名配置

在tsconfig.json文件中对ts进行配置,配置正确会有类型提示,这也是使用ts的好处。

// tsconfig.json
{
  "compilerOptions": {
    // ...
    "types": ["element-plus/global"],
    "baseUrl": "./", // 解析非相对模块的基础地址,默认是当前目录
    "paths": {"@/*": ["src/*"]},  // 路径映射,相对于baseUrl
    "allowSyntheticDefaultImports": true // 允许默认导入
  }
}

配置环境变量

  • 开发环境配置:.env.development

    # 变量必须以 VITE_ 为前缀才能暴露给外部读取
    VITE_APP_TITLE = 'oursBlog'
    VITE_APP_PORT = 3080
    VITE_APP_BASE_API = '/dev-api'
    VITE_APP_BASE_API_MOCK = 'https://mock.mengxuegu.com/mock/636eff7ef22edd4bbbcd9919/mmServer'
    
  • 生产环境配置:.env.production

    VITE_APP_TITLE = 'oursBlog'
    VITE_APP_PORT = 3080
    VITE_APP_BASE_API = '/prod-api'
    
  • 测试环境配置:.env.testing

    VITE_APP_TITLE = 'oursBlog'
    VITE_APP_PORT = 3080
    VITE_APP_BASE_API = '/testing-api'
    

Vite配置

官方文档:Home | Vite中文网 (vitejs.cn)

先安装ts的类型描述文件,再在vite.config.ts文件中配置代理服务可以解决跨域问题。

在tsconfig.node.json文件中配置基础路径和路径映射。

// 安装TypeScript类型描述文件
npm install @types/node -S --registry=https://registry.npm.taobao.org

// vite.config.ts
import { UserConfig, ConfigEnv, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default ({ command, mode }: ConfigEnv): UserConfig => {
  const env = loadEnv(mode, process.cwd())

  return {
    plugins: [ vue(),],
    server: {
      host: '0.0.0.0',
      port: Number(env.VITE_APP_PORT),
      open: true,
      proxy: {
        [env.VITE_APP_BASE_API]: {
          target: 'https://mock.mengxuegu.com/mock/636eff7ef22edd4bbbcd9919/mmServer',
          changeOrigin: true,
          rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
        }
      }
    },
    resolve: {
      alias: {
        '@': path.resolve('./src')
      }
    }
  }
}



// tsconfig.node.json
{
  "compilerOptions": {
    "composite": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
    "paths": { "@/*": ["src/*"] } //路径映射,相对于baseUrl
  },
  "include": ["vite.config.ts"]
}

自动导入插件

由于vue3的api是要先引入再使用的,这样每次都要引入就会很麻烦,于是相应的插件应运而生,一个是引入api的一个是引入组件的。

npm install unplugin-auto-import unplugin-vue-components -D --registry=https://registry.npm.taobao.org
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

plugins: [
  vue(),
  AutoImport({
    resolvers: [ElementPlusResolver()],
    include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
    imports: [
      // 插件预设支持导入的api
      'vue'
    ],
    dts: './auto-imports.d.ts'
  }),
  Components({
    resolvers: [ElementPlusResolver()]
  })
],

tsconfig.json文件中添加"./auto-imports.d.ts",否则识别不到会不生效。

// tsconfig.json

"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","./auto-imports.d.ts"],

启动项目测试一下,能否正常运行。

// App.vue








Pinia状态管理

新一代状态管理工具,好用!

1.pinia安装

npm install pinia --registry=https://registry.npm.taobao.org

2.pinia注册

// src/main.ts
import { createPinia } from "pinia"

createApp(App)
    .use(ElementPlus)
    .use(createPinia())
    .mount('#app')

3.pinia模块封装

// store/modules/user.ts

import { defineStore } from 'pinia'
import { UserState } from '../storeTypes'

const useUserStore = defineStore({
  id: 'user',
  state: (): UserState => ({
    token: '',
    roles: [],
    perms: []
  }),
  actions: {}
})

export default useUserStore

// src\store\storeTypes.ts

export interface UserState {
  token: string
  nickname?: string
  avatar?: string
  roles: string[]
  perms: string[]
}

// store/index.ts
import useUserStore from './modules/user';

const useStore = () => ({
  user: useUserStore(),
});

export default useStore;

Axios封装

1.安装axios

npm install axios --registry=https://registry.npm.taobao.org

2.浏览器缓存封装

// utils/storage.ts

// window.localStorage 浏览器永久缓存
export const localStorage = {
  // 设置永久缓存
  set(key: string, val: any) {
    window.localStorage.setItem(key, JSON.stringify(val));
  },
  // 获取永久缓存
  get(key: string) {
    const json: any = window.localStorage.getItem(key);
    return JSON.parse(json);
  },
  // 移除永久缓存
  remove(key: string) {
    window.localStorage.removeItem(key);
  },
  // 移除全部永久缓存
  clear() {
    window.localStorage.clear();
  }
};


// window.sessionStorage 浏览器临时缓存
export const sessionStorage = {
  // 设置临时缓存
  set(key: string, val: any) {
    window.sessionStorage.setItem(key, JSON.stringify(val));
  },
  // 获取临时缓存
  get(key: string) {
    const json: any = window.sessionStorage.getItem(key);
    return JSON.parse(json);
  },
  // 移除临时缓存
  remove(key: string) {
    window.sessionStorage.removeItem(key);
  },
  // 移除全部临时缓存
  clear() {
    window.sessionStorage.clear();
  }
};

3.请求封装

实际开发过程中接口不会很快就出来,所以mock接口是必要的,那么在封装的时候就可以考虑mock接口的功能加进去。

// src\utils\request.ts

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import { localStorage } from '../utils/storage'
import useStore from '../store'

const baseURLAll = ref(import.meta.env.VITE_APP_BASE_API)

type TOptions = {
  mock?: boolean // 是否启用mock
  mockUrl?: string // 自定义mock地址
}

const request = (options: TOptions & AxiosRequestConfig) => {
  if (options?.mock) baseURLAll.value = options.mockUrl ?? import.meta.env.VITE_APP_BASE_API_MOCK
  // 对当前环境进行二次判断 防止生产环境调用了测试环境接口------------------********待添加*********------------------

  const service = axios.create({
    baseURL: baseURLAll.value,
    timeout: 50000
  })

  // 请求拦截器
  service.interceptors.request.use(
    (config: AxiosRequestConfig) => {
      if (!config.headers) {
        throw new Error(`Expected 'config' and 'config.headers' not to be undefined`)
      }
      const { user } = useStore()
      if (user.token) {
        config.headers.Authorization = `${localStorage.get('token')}`
      }
      return config
    },
    (error) => {
      return Promise.reject(error)
    }
  )

  // 响应拦截器
  service.interceptors.response.use(
    (response: AxiosResponse) => {
      const { code, msg } = response.data
      if (code === '200') {
        return response.data
      } else {
        ElMessage({
          message: msg || '系统出错',
          type: 'error'
        })
        return Promise.reject(new Error(msg || 'Error'))
      }
    },
    (error) => {
      const { code, msg } = error.response.data
      if (code === 'A0230') {
        // token 过期
        localStorage.clear() // 清除浏览器全部缓存
        window.location.href = '/' // 跳转登录页
        ElMessageBox.alert('当前页面已失效,请重新登录', '提示', {})
          .then(() => {})
          .catch(() => {})
      } else {
        ElMessage({
          message: msg || '系统出错',
          type: 'error'
        })
      }
      return Promise.reject(new Error(msg || 'Error'))
    }
  )
  return service(options)
}

export default request

4.API封装

// src\api\user\index.ts

import request from '@/utils/request'
import { AxiosPromise } from 'axios'
import { UserInfo } from './types'

export function getUserInfo(data: { userId: Number | null }): AxiosPromise {
  return request({
    url: '/api/users/userInfo',
    method: 'post',
    data
  })
}

// src\api\user\types.ts

export interface UserInfo {
  nickname: string;
  avatar: string;
  roles: string[];
  perms: string[];
}

utils

// src\utils\index.ts

/**
 * Check if an element has a class
 * @param {HTMLElement} elm
 * @param {string} cls
 * @returns {boolean}
 */
export function hasClass(ele: HTMLElement, cls: string) {
  return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'));
}

/**
 * Add class to element
 * @param {HTMLElement} elm
 * @param {string} cls
 */
export function addClass(ele: HTMLElement, cls: string) {
  if (!hasClass(ele, cls)) ele.className += ' ' + cls;
}

/**
 * Remove class from element
 * @param {HTMLElement} elm
 * @param {string} cls
 */
export function removeClass(ele: HTMLElement, cls: string) {
  if (hasClass(ele, cls)) {
    const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)');
    ele.className = ele.className.replace(reg, ' ');
  }
}

export function mix(color1: string, color2: string, weight: number) {
  weight = Math.max(Math.min(Number(weight), 1), 0);
  const r1 = parseInt(color1.substring(1, 3), 16);
  const g1 = parseInt(color1.substring(3, 5), 16);
  const b1 = parseInt(color1.substring(5, 7), 16);
  const r2 = parseInt(color2.substring(1, 3), 16);
  const g2 = parseInt(color2.substring(3, 5), 16);
  const b2 = parseInt(color2.substring(5, 7), 16);
  const r = Math.round(r1 * (1 - weight) + r2 * weight);
  const g = Math.round(g1 * (1 - weight) + g2 * weight);
  const b = Math.round(b1 * (1 - weight) + b2 * weight);
  const rStr = ('0' + (r || 0).toString(16)).slice(-2);
  const gStr = ('0' + (g || 0).toString(16)).slice(-2);
  const bStr = ('0' + (b || 0).toString(16)).slice(-2);
  return '#' + rStr + gStr + bStr;
}

动态权限路由&router

1. 安装 vue-router

npm install vue-router@next nprogress @types/nprogress --registry=https://registry.npm.taobao.org

2. 创建路由实例

创建路由实例并导出,其中包括静态路由数据,动态路由后面将通过接口从后端获取并整合用户角色的权限控制。

// 新建src\layout\index.vue文件
// 新建src\views\error-page\401.vue文件
// 新建src\views\error-page\404.vue文件
// 新建src\views\login\index.vue文件
// 新建src\views\redirect\index.vue文件
// 新建src\views\dashboard\index.vue文件

// src/router/index.ts

import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import useStore from '@/store'

export const Layout = () => import('@/layout/index.vue')

// 静态路由
export const constantRoutes: Array = [
  {
    path: '/redirect',
    component: Layout,
    meta: { hidden: true },
    children: [
      {
        path: '/redirect/:path(.*)',
        component: () => import('@/views/redirect/index.vue')
      }
    ]
  },
  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    meta: { hidden: true }
  },
  {
    path: '/404',
    component: () => import('@/views/error-page/404.vue'),
    meta: { hidden: true }
  },
  {
    path: '/401',
    component: () => import('@/views/error-page/401.vue'),
    meta: { hidden: true }
  },
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        name: 'Dashboard',
        meta: { title: 'dashboard', icon: 'dashboard', affix: true }
      }
    ]
  }
]

// 创建路由实例
const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes as RouteRecordRaw[],
  // 刷新时,滚动条位置还原
  scrollBehavior: () => ({ left: 0, top: 0 })
})

// 重置路由
export function resetRouter() {
  // doing...
}

export default router

3. 路由实例全局注册

// main.ts
import router from "@/router";
// import '@/permission';

createApp(App).use(router).use(ElementPlus).use(createPinia()).mount('#app')
// App.vue







试一试!启动项目!成功一大半!

4.登录退出等api封装

auth

// src\api\auth\types.ts

/**
 * 登录表单类型声明
 */
export interface LoginForm {
  username: string
  password: string
  grant_type: string
  /**
   * 验证码Code
   */
  //verifyCode: string;
  /**
   * 验证码Code服务端缓存key(UUID)
   */
  // verifyCodeKey: string;
}

/**
 * 登录响应类型声明
 */
export interface LoginResult {
  access_token: string
  token_type: string
}

/**
 * 验证码类型声明
 */
export interface VerifyCode {
  verifyCodeImg: string
  verifyCodeKey: string
}

// src\api\auth\index.ts

import request from '@/utils/request'
import { AxiosPromise } from 'axios'
import { LoginForm, VerifyCode } from './types'

/**
 *
 * @param data {LoginForm}
 * @returns
 */
export function loginApi(data: LoginForm): AxiosPromise {
  return request({
    url: '/api/auth/login',
    method: 'post',
    params: data,
    headers: {
      Authorization: 'Basic dnVlMy1lbGVtZW50LWFkbWluOnNlY3JldA==' // 客户端信息Base64明文:vue3-element-admin:secret
    }
  })
}

/**
 * 注销
 */
export function logoutApi() {
  return request({
    url: '/api/auth/logout',
    method: 'delete'
  })
}

/**
 * 获取图片验证码
 */
export function getCaptcha(): AxiosPromise {
  return request({
    url: '/captcha?t=' + new Date().getTime().toString(),
    method: 'get'
  })
}

// src\api\menu\types.ts

/**
 * 菜单查询参数类型声明
 */
export interface MenuQuery {
  keywords?: string
}

/**
 * 菜单分页列表项声明
 */

export interface Menu {
  id?: number
  parentId: number
  type?: string | 'CATEGORY' | 'MENU' | 'EXTLINK'
  createTime: string
  updateTime: string
  name: string
  icon: string
  component: string
  sort: number
  visible: number
  children: Menu[]
}

/**
 * 菜单表单类型声明
 */
export interface MenuForm {
  //菜单ID
  id?: string
  //父菜单ID
  parentId: string
  //菜单名称
  name: string
  //菜单是否可见(1:是;0:否;)
  visible: number
  icon?: string
  //排序
  sort: number
  //组件路径
  component?: string
  //路由路径
  path: string
  //跳转路由路径
  redirect?: string
  //菜单类型
  type: string
  //权限标识
  perm?: string
}

/**
 * 资源(菜单+权限)类型
 */
export interface Resource {
  // 菜单值
  value: string
  //菜单文本
  label: string
  //子菜单
  children: Resource[]
}

/**
 * 权限类型
 */
export interface Permission {
  // 权限值
  value: string
  //权限文本
  label: string
}


// src\api\menu\index.ts

import request from '@/utils/request'
import { AxiosPromise } from 'axios'
import { MenuQuery, Menu, Resource, MenuForm } from './types'

type OptionType = {
  value: string
  label: string
  checked?: boolean
  children?: OptionType[]
}

/**
 * 获取路由列表
 */
export function listRoutes() {
  return request({
    url: '/api/v1/menus/routes',
    method: 'get'
  })
}

/**
 * 获取菜单表格列表
 *
 * @param queryParams
 */
export function listMenus(queryParams: MenuQuery): AxiosPromise {
  return request({
    url: '/api/v1/menus',
    method: 'get',
    params: queryParams
  })
}

/**
 * 获取菜单下拉树形列表
 */
export function listMenuOptions(): AxiosPromise {
  return request({
    url: '/api/v1/menus/options',
    method: 'get'
  })
}

/**
 * 获取资源(菜单+权限)树形列表
 */
export function listResources(): AxiosPromise {
  return request({
    url: '/api/v1/menus/resources',
    method: 'get'
  })
}

/**
 * 获取菜单详情
 * @param id
 */
export function getMenuDetail(id: string): AxiosPromise {
  return request({
    url: '/api/v1/menus/' + id,
    method: 'get'
  })
}

/**
 * 添加菜单
 *
 * @param data
 */
export function addMenu(data: MenuForm) {
  return request({
    url: '/api/v1/menus',
    method: 'post',
    data: data
  })
}

/**
 * 修改菜单
 *
 * @param id
 * @param data
 */
export function updateMenu(id: string, data: MenuForm) {
  return request({
    url: '/api/v1/menus/' + id,
    method: 'put',
    data: data
  })
}

/**
 * 批量删除菜单
 *
 * @param ids 菜单ID,多个以英文逗号(,)分割
 */
export function deleteMenus(ids: string) {
  return request({
    url: '/api/v1/menus/' + ids,
    method: 'delete'
  })
}

5.store公共方法

// src\store\storeTypes.ts
import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';

export interface AppState {
  device: string;
  sidebar: {
    opened: boolean;
    withoutAnimation: boolean;
  };
  language?: string;
  size: string;
}

export interface PermissionState {
  routes: RouteRecordRaw[];
  addRoutes: RouteRecordRaw[];
}

export interface SettingState {
  theme: string;
  tagsView: boolean;
  fixedHeader: boolean;
  showSettings: boolean;
  sidebarLogo: boolean;
}

export interface UserState {
  token: string;
  nickname: string;
  avatar: string;
  roleList?: string[];
  perms: string[];
  roles: string[];
  userId: Number | null;
}

export interface TagView extends Partial {
  title?: string;
}

export interface TagsViewState {
  visitedViews: TagView[];
  cachedViews: string[];
}

// src/store/modules/user.ts 
import { defineStore } from 'pinia'
import { UserState } from '../storeTypes'
import { localStorage } from '@/utils/storage'
import { loginApi, logoutApi } from '@/api/auth'
import { getUserInfo } from '@/api/user'
import { resetRouter } from '@/router'
import { LoginForm } from '@/api/auth/types'

const useUserStore = defineStore({
  id: 'user',
  state: (): UserState => ({
    token: localStorage.get('token') || '',
    nickname: '',
    avatar: '',
    userId: null,
    roles: [],
    perms: []
  }),
  actions: {
    //  重置仓库到初始状态
    async RESET_STATE() {
      this.$reset()
    },

    // 登录
    login(data: LoginForm) {
      const { username, password } = data
      return new Promise((resolve, reject) => {
        loginApi({
          grant_type: 'password',
          username: username.trim(),
          password: password
        })
          .then((response: { data: any }) => {
            const { userId, token } = response.data
            localStorage.set('token', token)
            this.token = token
            this.userId = userId
            resolve(token)
          })
          .catch((error) => {
            reject(error)
          })
      })
    },

    //  获取用户信息(昵称、头像、角色集合、权限集合)
    getUserInfo() {
      return new Promise((resolve, reject) => {
        getUserInfo({ userId: this.userId })
          .then(({ data }) => {
            if (!data) {
              return reject('Verification failed, please Login again.')
            }
            const { nickname, avatar, roles, perms } = data
            if (!roles || roles.length <= 0) {
              reject('getUserInfo: roles must be a non-null array!')
            }
            this.nickname = nickname
            this.avatar = avatar
            this.roles = roles
            this.perms = perms
            resolve(data)
          })
          .catch((error) => {
            reject(error)
          })
      })
    },

    //  注销
    logout() {
      return new Promise((resolve, reject) => {
        logoutApi()
          .then(() => {
            localStorage.remove('token')
            this.RESET_STATE()
            resetRouter()
            resolve(null)
          })
          .catch((error) => {
            reject(error)
          })
      })
    },

    //  清除 Token
    resetToken() {
      return new Promise((resolve) => {
        localStorage.remove('token')
        this.RESET_STATE()
        resolve(null)
      })
    }
  }
})

export default useUserStore

// src\store\modules\permission.ts

import { PermissionState } from '../storeTypes'
import { RouteRecordRaw } from 'vue-router'
import { defineStore } from 'pinia'
import { constantRoutes } from '@/router'
import { listRoutes } from '@/api/menu'

const modules = import.meta.glob('../../views/**/**.vue')
export const Layout = () => import('@/layout/index.vue')

const hasPermission = (roles: string[], route: RouteRecordRaw) => {
  if (route.meta && route.meta.roles) {
    if (roles.includes('ROOT')) {
      return true
    }
    return roles.some((role) => {
      if (route.meta?.roles !== undefined) {
        return (route.meta.roles as string[]).includes(role)
      }
    })
  }
  return false
}

// 角色过滤路由
export const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
  const res: RouteRecordRaw[] = []
  routes.forEach((route) => {
    const tmp = { ...route } as any
    if (hasPermission(roles, tmp)) {
      if (tmp.component == 'Layout') {
        tmp.component = Layout
      } else {
        const component = modules[`../../views/${tmp.component}.vue`] as any
        if (component) {
          tmp.component = component
        } else {
          tmp.component = modules[`../../views/error-page/404.vue`]
        }
      }
      res.push(tmp)

      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
    }
  })
  return res
}

const usePermissionStore = defineStore({
  id: 'permission',
  state: (): PermissionState => ({
    routes: [], // 静态路由 + 动态路由
    addRoutes: [] //动态路由
  }),
  actions: {
    setRoutes(routes: RouteRecordRaw[]) {
      this.addRoutes = routes
      this.routes = constantRoutes.concat(routes)
    },
    generateRoutes(roles: string[]) {
      return new Promise((resolve, reject) => {
        listRoutes()
          .then((response) => {
            const asyncRoutes = response.data
            const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
            this.setRoutes(accessedRoutes)
            resolve(accessedRoutes)
          })
          .catch((error) => {
            reject(error)
          })
      })
    }
  }
})

export default usePermissionStore

// src\store\index.ts

import useUserStore from './modules/user'
import usePermissionStore from './modules/permission'

const useStore = () => ({
  user: useUserStore(),
  permission: usePermissionStore(),
})

export default useStore

6. 动态权限路由,路由鉴权,鉴权文件引入

// main.ts引入

// src/permission.ts
import router from '@/router'
import { ElMessage } from 'element-plus'
import useStore from '@/store'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
NProgress.configure({ showSpinner: false }) // 进度环显示/隐藏

// 白名单路由
const whiteList = ['/login']

router.beforeEach(async (to, form, next) => {
  NProgress.start()
  const { user, permission } = useStore()
  const hasToken = user.token
  // 有token
  if (hasToken) {
    // 登录成功,跳转到首页
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
      const hasGetUserInfo = user.roles.length > 0
      // 有用户信息
      if (hasGetUserInfo) {
        next()
      } else {
        // 无用户信息,重新获取用户信息路由信息
        try {
          await user.getUserInfo()
          const roles = user.roles
          // 用户拥有权限的路由集合(accessRoutes)
          // 是根据用户角色获取拥有权限的路由(静态路由+动态路由)
          const accessRoutes: any = await permission.generateRoutes(roles)
          accessRoutes.forEach((route: any) => {
            router.addRoute(route)
          })
          next({ ...to, replace: true })
        } catch (error) {
          // 移除 token 并跳转登录页
          await user.resetToken()
          ElMessage.error((error as any) || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    // 未登录可以访问白名单页面(登录页面),无token
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  NProgress.done()
})

启动项目测试权限控制

SVG图标

1. 安装 vite-plugin-svg-icons

npm i [email protected] [email protected] -D --registry=https://registry.npm.taobao.org

2. 创建图标文件夹

​ 项目创建 src/assets/icons 文件夹,存放 iconfont 下载的 SVG 图标

3. main.ts 引入注册脚本

// main.ts
import 'virtual:svg-icons-register';

4. vite.config.ts 插件配置

// vite.config.ts
import {UserConfig, ConfigEnv, loadEnv} from 'vite'
import vue from '@vitejs/plugin-vue'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';

export default ({command, mode}: ConfigEnv): UserConfig => {
    // 获取 .env 环境配置文件
    const env = loadEnv(mode, process.cwd())

    return (
        {
            plugins: [
                //...
                createSvgIconsPlugin({
                    // 指定需要缓存的图标文件夹
                    iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
                    // 指定symbolId格式
                    symbolId: 'icon-[dir]-[name]',
                })
            ]
        }
    )
}

5. TypeScript支持

// tsconfig.json
{
  "compilerOptions": {
    "types": ["vite-plugin-svg-icons/client"]
  }
}

6. 组件封装







7. 使用示例



  

样式文件

// src\styles\index.scss

body {
  margin: 0;
  padding: 0;
  height: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
    Microsoft YaHei, Arial, sans-serif;
}

label {
  font-weight: 700;
}

html {
  height: 100%;
  box-sizing: border-box;
}

#app {
  height: 100%;
}

*,
*:before,
*:after {
  box-sizing: inherit;
}

a:focus,
a:active {
  outline: none;
}

a,
a:focus,
a:hover {
  cursor: pointer;
  color: inherit;
  text-decoration: none;
}

div:focus {
  outline: none;
}

.clearfix {
  &:after {
    visibility: hidden;
    display: block;
    font-size: 0;
    content: ' ';
    clear: both;
    height: 0;
  }
}

// main-container global css
.app-container {
  padding: 20px;
}

.search{
  padding:18px  0 0  10px;
  margin-bottom: 10px;
  box-shadow: var(--el-box-shadow-light);
  border-radius: var(--el-card-border-radius);
  border: 1px solid var(--el-card-border-color);
}


登录页面

//src\views\login\index.vue









!!!到这里一个管理系统最基础的部分就算完成了,如果项目创新程度比较高可以从这里开始:refreshing分支。
如果要做一个比较传统的管理后台可以选择master分支!!!

layout布局组件

传统基础布局

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2VgbQpcd-1670077065047)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/10ae7707281c49599c77000226472d56~tplv-k3u1fbpfcp-watermark.image?)]

按钮权限

1. Directive 自定义指令

// src/directive/permission/index.ts

import useStore from "@/store";
import { Directive, DirectiveBinding } from "vue";

/**
 * 按钮权限校验
 */
export const hasPerm: Directive = {
    mounted(el: HTMLElement, binding: DirectiveBinding) {
        // 「超级管理员」拥有所有的按钮权限
        const { user } = useStore()
        const roles = user.roles;
        if (roles.includes('ROOT')) {
            return true
        }
        // 「其他角色」按钮权限校验
        const { value } = binding;
        if (value) {
            const requiredPerms = value; // DOM绑定需要的按钮权限标识

            const hasPerm = user.perms.some(perm => {
                return requiredPerms.includes(perm)
            })

            if (!hasPerm) {
                el.parentNode && el.parentNode.removeChild(el);
            }
        } else {
            throw new Error("need perms! Like v-has-perm="['sys:user:add','sys:user:edit']"");
        }
    }
};

2. 自定义指令全局注册

// src/main.ts

const app = createApp(App)
// 自定义指令
import * as directive from "@/directive";

Object.keys(directive).forEach(key => {
    app.directive(key, (directive as { [key: string]: Directive })[key]);
});

3. 指令使用

// src/views/system/user/index.vue
新增
删除

你可能感兴趣的:(vue.js,javascript,前端)