zl程序教程

您现在的位置是:首页 >  前端

当前栏目

Vue后台管理系统开发,相关代码的笔记。

2023-06-13 09:17:02 时间

开发记录

从零开始开发后台管理系统,还是有很多值得记录的地方。构建工具Vite、使用Vue3。

1.批量导入指定目录的组件

/*
* @author 友人a丶
* @date 2022-07-11
* @app Vue应用对象
* */
export default function (app) { 
    /* 
    * 指定要导入的文件目录 
    * 直接加载用{eager:true},懒加载用glob 
    * */
    const modules = import.meta.glob(['@/layouts/*/index.js', '@/components/*/index.js']);
    
    for (let i in modules) {
        let name = /(.*)?\/(.*)\/index.js/.exec(i);
        /*直接引入组件*/
        app.component(name[2], modules[i].default);
        /*异步组件*/
        app.component(name[2], defineAsyncComponent(modules[i]));
    }
}

直接加载的时候modules就是元素为组件对象的数组,懒加载的时候是元素为import方法的函数数组 。

2.应用初始化

/*
* @author 友人a丶
* @date 2022-07-11
*
* 引导系统初始化
* 初始化全局响应拦截器
* 初始化路由守卫
* 初始化用户登录
* */

import loadGuard from "./loadGuard"
import loadInterceptor from "./loadInterceptor"
import watchRem from "./watchRem";

export default async function (){

    /*
    * 自动调整rem
    * */
    watchRem();
    /*
    * 加载拦截器
    * */
    loadInterceptor();
    /*
    * 加载路由守卫
    * */
    loadGuard();
}

3.自动调整rem

/*
* @author 友人a丶
* @date 2022-07-11
* 自动调整rem的大小
* */

export default function () {
    let l = () => {
        let r = document.documentElement, o = r.offsetWidth / 100;
        o < 16 && (o = 16), r.style.fontSize = o + "px", window.rem = o
    };

    l();
    window.addEventListener("resize",()=>l());
}

4.导航守卫

/*
* @author 友人a丶
* @date 2022-07-11
*
* 加载全局路由守卫
* */


import {router} from "@/router";
import userStore from "@/stores/user";
import load from "@/common/load";
import systems from "@/stores/system";
import NProgress from 'nprogress'
import loadUser from "@/service/loadUser";


/*进度条*/
NProgress.configure({showSpinner: false})


// 不需要拦截的路由配置
const ignoreRoute = {
    names: ['404', '403'],      //根据路由名称匹配
    paths: ['/login'],   //根据路由fullPath匹配
    /**
     * 判断路由是否包含在该配置中
     * @param route vue-router 的 route 对象
     * @returns {boolean}
     */
    includes(route) {
        return ignoreRoute.names.includes(route.name) || ignoreRoute.paths.includes(route.path)
    }
}


/*
* 加载路由守卫
* */
export default function () {

    console.log("加载路由守卫...");
    let user = userStore();//全局状态


    /*
    * 加载进度条
    * */
    router.beforeEach((to, from, next) => {
        // start progress bar
        if (!NProgress.isStarted()) {
            NProgress.start()
        }
        next()
    });

    /*
    * 判断系统是否初始化
    * */
    router.beforeEach(async (to, from, next) => {
        if(!systems().loaded){
            await loadUser();//加载用户信息初始化系统
        }
        next(); //下一个
    });



    /*
    * 已登录时,访问登录页面,让它走
    * */
    router.beforeEach( (to, from, next) => {
        if (user.role != 0 && (to.path == "/login")) {
            next('/');
        }else{
            next(); //下一个
        }
    });



    /*
    * 判断是否登录
    * */
    router.beforeEach((to, from, next) => {

        /*
        * 判断是否需要拦截
        * */
        console.log("登录判断守卫激活....")

        if (ignoreRoute.includes(to)) {
            next();
        } else {
            /*
            * 角色为0,代表未登录
            * */
            if (user.role == 0) {
                next({path: '/login'});
            } else {
                next();
            }
        }
    })


    /*
    * 判断用户权限
    * */
    router.beforeEach((to, from, next) => {

        console.log("权限判断守卫激活....")
        /*
        * 判断是否需要拦截
        * */
        if (ignoreRoute.includes(to)) {
            next();
        } else {
            /*
            * 判断用户权限
            * */
            if (user.role < to.meta.role) {
                load.error("您无权限访问该页面....");
                next({path: '/403'});
            } else {
                next();
            }
        }
    })


    /*
    * 切换页面标题
    * */
    router.beforeEach((to, from, next) => {
        document.title = to.name
        next();
    })


    /*
    * 结束进度条
    * */
    router.afterEach(() => {
        // finish progress bar
        NProgress.done()
    });

}

5.axios拦截器 

/*
* @author 友人a丶
* @date 2022-07-11
*
* 加载axio拦截器
* */
import axios from "axios";
import load from "@/common/load";
import {router} from "@/router";
import apis from '@/service/api';

// 不需要拦截的接口
const ignoreApi = {
    api: [
        apis.login
    ],
    includes(api) {

        /*
        * 判断当前请求的接口是否在忽略的列表
        * */
        for(let item of ignoreApi.api){
            let reg=new RegExp(`.*${item}.*`);
            if(reg.test(api)){
                return true;
            }
        }
        return false;
    }
}

/*
* 注册响应拦截器
* */
export default function (){
    console.log("加载拦截器...");

    axios.interceptors.response.use(function (res){
        console.log("请求接口:"+res.config.url)
        console.log(res);

        /*
        * 判断是否需要拦截
        * */
        if(ignoreApi.includes(res.config.url)){
            return res;
        }

        /*
        * 判断用户登录是否失效
        * */
        if(res.code == -1){
            load.confirm("当前登录状态已失效,请您重新登录!",()=>{
                router.replace('/login')
            })
        }

        return res;
    });


}

6.获取某个路由的子路由(用于生成菜单)

/*
* 操作路由的相关方法
* */
import {router} from "@/router/index";


/*
* 获取某个路由项的子项
* */
export function getChildren(path) {

    let routes=router.getRoutes();

    for (let i of routes) {
        if (i.path == path) {
            return i.children;
        }
    }
}

7.简单的弹出封装(antd design vue)

import {
	message,
	Modal
} from "ant-design-vue";


let hide = [];

export default {

	loading(text = '加载中...') {
		hide.push(message.loading(text, 0))
	},
	loaded() {
		if (hide.length > 0) {

			let timer=setTimeout(()=>{
				hide[hide.length - 1]()
				hide.splice(hide.length - 1, 1)
			},500);

		}
	},
	error(text = '加载异常') {
		message.error(text);
	},
	success(text = 'ok!') {
		message.success(text);
	},
	confirm(text, callback = null) {
		Modal.confirm({
			title: '提示',
			centered: true,
			content: text,
			maskClosable: false,
			onOk: (close) => {
				close(); //关闭
				if (callback) {
					callback()
				}
			}
		})
	}
}

8.退出登录

/*
* @author 友人a丶
* @date 2022-07-11
* 用户退出登录
* */


import user from "@/stores/user";
import Cookies from "js-cookie";
import {router} from "@/router";
import load from "@/common/load";


export default function () {

    load.confirm("确认退出登录吗?",()=>{
        user().$reset(); //重置用户数据的状态管理器
        /*
        * 清空cookie
        * */
        Object.keys(Cookies.get()).forEach((item)=>{
            Cookies.remove(item);
        })
        /*
        * 跳转登录界面
        * */
        router.replace('/login');
    })

}

9.获取需要缓存的组件列表

/*
* @author 友人a丶
* @date 2022-07-11
* name代表组件名
* 获取需要缓存的组件
* */

import routes from '@/router'

export function getChached(path='') {
    let routes=router.getRoutes();
    let cahced=[]; //是否开启缓存
    /*
    * 为空代表获取所有一级组件
    * */
    if(path == ""){
        routes.forEach((item)=>{
            if(item.meta.cache){
                cahced.push(item.meta.cache);
            }
        });
    }else{
        /*遍历目标子组件*/
        for (let i of routes) {
            if (i.path == path) {
                i.children.forEach((item)=>{
                    if(item.meta.cache){
                        cahced.push(item.meta.cache);
                    }
                });
            }
        }
    }

    return cahced;
}

需要考虑

1.如何让显示的菜单响应路由的变化(跳转到某个页面,自动选中某个菜单)?

本身菜单被点击了,自己会变化被选中的状态,需要考虑的是从其他页面跳转过来的时候,如何正常匹配显示被选的菜单;

路由包括静态的路由和有变化的参数路由,某些情况下还会具有参数。

router.matched,与给定路由地址匹配的标准化的路由记录数组。

正则匹配,搭配计算属性;假设业务场景:【顶部是一级菜单,用于打开一个新页面,每个页面都有自身的菜单(二级菜单),菜单下面加包括子菜单】,首先就需要根据上方一级菜单的变化匹配二级菜单,还需要根据当前路由判断哪个子菜单被选中;所有需要条件有:代表当前路由的响应式变量、代表当前一级路由的子路由的响应式变量、代表被选中的菜单的响应式变量,最终如下:

let selectedKeys = computed({
      get() {
        let current = route.fullPath;

        for (let i = 0; i < items.length; i++) {
          //断言右边是空或者?或者/
          // 完整匹配或者带参数匹配
          let regexp=new RegExp(`(?:.*${items[i].path}$)|(?:.*${items[i].path}[\?\/].*)`);
          if(regexp.test(current)){
            return [i];
          }
        }
      },
      set(value) {
        return;
      }
    }
);

考虑到参数路由和带有?相关参数的路由,所以正则匹配的是 当前路由与对应的路由完全相等以及部分相等的同时右边为/或者?

提示

由此还需考虑父路由存在相似的路由片段时,匹配的优先级的问题

2.如何组织目录?

  • 代表页面的组件一般以文件夹的形式通过index.js导出组件,方便观察层次结构,并且页面组件一般都会拆分JS模块,通过文件夹也更加方便文件的分类,保持目录的简洁。
  • 其他的组件,如果设计到大量的逻辑,需要拆分JS模块,可以用文件夹,如何很简单的直接用.vue文件即可。
  • 如何让父子组件的层级更加清晰?首先名字可以按层级写;parent-children.vue。
  • 名字较长的组件用“-”分割,更加友好。

3.结构型的组件划分?

  • 将布局看组架子(布局组件)、视图看做需要的内容(视图组件),布局承载内容;
  • 通过全局状态的设置来动态调整布局组件的显示和隐藏。

4.如何组织无限层级的子路由作为菜单?

模板方式实现起来非常的麻烦,JSX的方式更加适合这种需求;

  1. 首先需要根据当前路由获取一个可以作为祖先的父级路由对象

5.运行中的router

getRoutes(); 获取一个包括所有路由项的数组;不同层次的路由path属性都是从根节点开始的;路由的children属性则是内的子路由是相对路径

不管是push、redirect、route-link,都可以进行相对路径(dynamic)或者绝对路径(/dynamic)跳转;

[
    {
        "path": "/spread/tencent",
        "name": "腾讯广告",
        "meta": {
            "role": 0,
            "icon": "icon-guangdiantong"
        },
        "props": {
            "default": false
        },
        "children": [],
        "instances": {},
        "leaveGuards": {},
        "updateGuards": {},
        "enterCallbacks": {},
        "components": {}
    }
]