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

23,184次阅读
没有评论

共计 6188 个字符,预计需要花费 16 分钟才能阅读完成。

开发记录

从零开始开发后台管理系统,还是有很多值得记录的地方。构建工具 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": {}}
]

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于2022-07-22发表,共计6188字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)