九、Vue3+Ts+Vite+AntdUI构建后台基础模板——动态添加路由

167 阅读3分钟

一、去除登录页浏览器的输入框提示

  1. 修改src/views/Login/index.vue
<!-- 输入框加入属性autocomplete="off"  -->
<template>
    <a-row class="layout" type="flex" justify="center" align="middle">
        <a-card class="login_card" :bodyStyle="{ height: '100%', padding: 'unset' }" hoverable>
            <div class="card_body">
                ......
                <div class="login_form">
                    <a-form class="form" ref="loginFormRef" layout="vertical" :rules="loginRules" :model="loginForm">
                        <a-form-item label="账号:" name="username">
                            <a-input v-model:value="loginForm.username" size="large" autocomplete="off" />
                        </a-form-item>
                        <a-form-item label="密码:" name="password">
                            <a-input-password v-model:value="loginForm.password" size="large" autocomplete="off" />
                        </a-form-item>
                        <a-form-item>
                            <a-button type="primary" size="large" block @click="onSubmit">提交</a-button>
                        </a-form-item>
                    </a-form>
                </div>
            </div>
        </a-card>
    </a-row>
</template>
<script lang="ts" setup>......</script>
<style lang="less" scoped>......</style>

二、新建需要的几个页面

新建404页面

  1. 在src/views新建目录404
  2. 在src/views/404目录新建文件index.vue
<template>
    <div class="page">
        <h1 data-t="404">404</h1>
        <p>当前页面不存在,<router-link to="/">点击回到首页</router-link></p>
    </div>
</template>

<script lang="ts" setup></script>

<style lang="less" scoped>
.page {
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    bottom: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

h1 {
    text-align: center;
    width: 100%;
    font-size: 6rem;
    animation: shake 0.6s ease-in-out infinite alternate;
}

@keyframes shake {
    0% {
        transform: translate(-1px);
    }

    10% {
        transform: translate(2px, 1px);
    }

    30% {
        transform: translate(-3px, 2px);
    }

    35% {
        transform: translate(2px, -3px);
        filter: blur(4px);
    }

    45% {
        transform: translate(2px, 2px) skewY(-8deg) scaleX(0.96);
        filter: blur(0);
    }

    50% {
        transform: translate(-3px, 1px);
    }
}

h1:before {
    content: attr(data-t);
    position: absolute;
    left: 50%;
    transform: translate(-50%, 0.34em);
    height: 0.1em;
    line-height: 0.5em;
    width: 100%;
    animation: scan 0.5s ease-in-out 275ms infinite alternate, glitch-anim 0.3s ease-in-out infinite alternate;
    overflow: hidden;
    opacity: 0.7;
}

@keyframes glitch-anim {
    0% {
        clip: rect(32px, 9999px, 28px, 0);
    }

    10% {
        clip: rect(13px, 9999px, 37px, 0);
    }

    20% {
        clip: rect(45px, 9999px, 33px, 0);
    }

    30% {
        clip: rect(31px, 9999px, 94px, 0);
    }

    40% {
        clip: rect(88px, 9999px, 98px, 0);
    }

    50% {
        clip: rect(9px, 9999px, 98px, 0);
    }

    60% {
        clip: rect(37px, 9999px, 17px, 0);
    }

    70% {
        clip: rect(77px, 9999px, 34px, 0);
    }

    80% {
        clip: rect(55px, 9999px, 49px, 0);
    }

    90% {
        clip: rect(10px, 9999px, 2px, 0);
    }

    to {
        clip: rect(35px, 9999px, 53px, 0);
    }
}

@keyframes scan {
    0%,
    20%,
    to {
        height: 0;
        transform: translate(-50%, 0.44em);
    }

    10%,
    15% {
        height: 1em;
        line-height: 0.2em;
        transform: translate(-55%, 0.09em);
    }
}

h1:after {
    content: attr(data-t);
    position: absolute;
    top: -8px;
    left: 50%;
    transform: translate(-50%, 0.34em);
    height: 0.5em;
    line-height: 0.1em;
    width: 100%;
    animation: scan 665ms ease-in-out 0.59s infinite alternate, glitch-anim 0.3s ease-in-out infinite alternate;
    overflow: hidden;
    opacity: 0.8;
}
</style>

新建Root页面

  1. 在src/views新建目录Root
  2. 在src/views/Root目录新建文件index.vue
<template>
    <h1>Root 权限才能看到的页面!</h1>
</template>

<script lang="ts" setup></script>

新建Admin页面

  1. 在src/views新建目录Admin
  2. 在src/views/Admin目录新建文件index.vue
<template>
    <h1>Admin 权限才能看到的页面!</h1>
</template>

<script lang="ts" setup></script>

新建Menu页面

  1. 在src/views新建目录Menu
  2. 在src/views/Menu目录新建文件index.vue
<template>
    <router-view />
</template>

新建Menu/Menu1页面

  1. 在src/views/Menu新建目录Menu1
  2. 在src/views/Menu/Menu1目录新建文件index.vue
<template>
    <h1>Menu1页面!</h1>
</template>

<script lang="ts" setup></script>

新建Menu/Menu2页面

  1. 在src/views/Menu新建目录Menu2
  2. 在src/views/Menu/Menu2目录新建文件index.vue
<template>
    <h1>Menu2页面!</h1>
</template>

<script lang="ts" setup></script>

三、修改mock中的login接口

  1. mock目录下新建data目录
  2. mock/data目录下新建文件routes.ts
  3. mock/data/routes.ts
interface routeConfig {
    path: string;
    name: string;
    component?: string;
    redirect?: string;
    meta: {
        title: string;
        layout: boolean;
    };
    children?: routeConfig[];
}

const rootRoutes: routeConfig[] = [
    {
        path: '/root',
        name: 'root',
        component: 'views/Root/index.vue',
        meta: {
            title: 'Root权限页',
            layout: true,
        },
    },
    {
        path: '/menu',
        name: 'menu',
        component: 'views/Menu/index.vue',
        redirect: '/menu/menu1',
        meta: {
            title: '多级菜单页',
            layout: true,
        },
        children: [
            {
                path: 'menu1',
                name: 'menu1',
                component: 'views/Menu/Menu1/index.vue',
                meta: {
                    title: 'Menu1页',
                    layout: true,
                },
            },
            {
                path: 'menu2',
                name: 'menu2',
                component: 'views/Menu/Menu2/index.vue',
                meta: {
                    title: 'Menu2页',
                    layout: true,
                },
            },
        ],
    },
];

const adminRoutes: routeConfig[] = [
    {
        path: '/admin',
        name: 'admin',
        component: 'views/Admin/index.vue',
        meta: {
            title: 'Admin权限页',
            layout: true,
        },
    },
];

const userRoutes: routeConfig[] = [];

export { rootRoutes, adminRoutes, userRoutes, routeConfig };
  1. 修改mock/login.ts
import Mock from 'mockjs';
import { rootRoutes, adminRoutes, userRoutes, routeConfig } from './data/routes';

export default [
    {
        // http://mockjs.com/examples.html
        url: '/mock/api/login',
        method: 'post',
        timeout: 500,
        // statusCode: 500,
        response: ({ body }) => {
            let role: string;
            let routes: routeConfig[] = [];
            if (body.username === 'root') {
                role = 'Root';
                routes = rootRoutes;
            } else if (body.username === 'admin') {
                role = 'Admin';
                routes = adminRoutes;
            } else {
                role = 'User';
                routes = userRoutes;
            }
            return {
                code: 200,
                success: true,
                message: 'ok',
                data: {
                    token: Mock.Random.string('lower', 200),
                    userInfo: {
                        id: Mock.Random.id(),
                        name: Mock.Random.cname(),
                        email: Mock.Random.email(),
                        gender: Mock.Random.natural(1, 2),
                        age: Mock.Random.natural(18, 30),
                        avatar: Mock.Random.image('800x800'),
                        role,
                    },
                    routes,
                },
            };
        },
    },
];

四、修改src/store/modules/user.ts

import { defineStore } from 'pinia';

const useUserStore = defineStore('user', {
    state: () => {
        return {
            token: '',
            userInfo: {},
            routes: [],
        };
    },
    actions: {
        ......
        setRoutes(routes) {
            this.routes = routes;
        },
        ......
        clearRoutes() {
            this.routes = [];
        },
    },
    persist: {
        enabled: true,
        strategies: [
            { key: 'token', storage: localStorage, paths: ['token'] },
            { key: 'userInfo', storage: localStorage, paths: ['userInfo'] },
            { key: 'routes', storage: localStorage, paths: ['routes'] },
        ],
    },
});

export default useUserStore;

五、在登录成功后存储动态路由数据

  1. 修改src/views/Login/index.vue
<template>
    ......
</template>

<script lang="ts" setup>
import { reactive, ref, toRaw } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/store';
import LoginAPI from '@/request/api/loginAPI';

interface loginFormConfig {
    username: string;
    password: string;
}

interface loginResConfig {
    token: string;
    userInfo: any;
}

const router = useRouter();
const userStore = useUserStore();

const loginFormRef = ref();
const loginForm: loginFormConfig = reactive({
    username: '',
    password: '',
});
const loginRules = {
    username: [
        {
            required: true,
            message: '请输入用户名',
            trigger: 'blur',
        },
    ],
    password: [
        {
            required: true,
            message: '请输入密码',
            trigger: 'blur',
        },
    ],
};

const onSubmit = () => {
    loginFormRef.value.validate().then(async () => {
        const res = (await LoginAPI.setLogin(toRaw(loginForm))) as loginResConfig;
        if (res) {
            console.log(res);
            userStore.setToken(res.token);
            userStore.setUserInfo(res.userInfo);
            userStore.setRoutes(res.routes);
            router.push({ path: '/' });
        }
    });
};
</script>

<style lang="less" scoped>
......
</style>

六、新建动态增删路由的公共方法

  1. src目录下新建util
  2. src/util目录下新建文件anyncRoutes.ts
// 递归遍历路由数据
const recursiveRoutes = (tree: any[], views) => {
    return tree.map((node) => {
        const tempNode = node;
        if (tempNode.component) {
            tempNode.component = views[`../${tempNode.component}`];
        }
        if (tempNode.children && tempNode.children.length > 0) {
            recursiveRoutes(tempNode.children, views);
        }
        return tempNode;
    });
};

// 添加动态路由
const addRoutes = (userStore, router) => {
    if (userStore.routes && userStore.routes.length > 0) {
        const routesData = JSON.parse(JSON.stringify(userStore.routes));
        const views = import.meta.glob('../views/**/*.vue');
        recursiveRoutes(routesData, views);
        routesData.forEach((item: any) => {
            router.addRoute(item);
        });
    }
    router.addRoute({
        path: '/:pathMatch(.*)*',
        name: '404',
        component: () => import('@/views/404/index.vue'),
        meta: {
            title: '无法找到该页面',
            layout: false,
        },
    });
};

// 清除动态路由
const clerRoutes = (userStore, router) => {
    if (userStore.routes && userStore.routes.length > 0) {
        userStore.routes.forEach((item: any) => {
            router.removeRoute(item.name);
        });
        userStore.clearRoutes();
    }
};

export { addRoutes, clerRoutes };

七、在登录成功后动态添加路由

  1. 修改src/views/Login/index.vue
<template>
    ......
</template>

<script lang="ts" setup>
import { reactive, ref, toRaw } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/store';
import LoginAPI from '@/request/api/loginAPI';
import { addRoutes } from '@/util/anyncRoutes';

interface loginFormConfig {
    username: string;
    password: string;
}

interface loginResConfig {
    token: string;
    userInfo: any;
    routes: [];
}

const router = useRouter();
const userStore = useUserStore();

const loginFormRef = ref();
const loginForm: loginFormConfig = reactive({
    username: '',
    password: '',
});
const loginRules = {
    username: [
        {
            required: true,
            message: '请输入用户名',
            trigger: 'blur',
        },
    ],
    password: [
        {
            required: true,
            message: '请输入密码',
            trigger: 'blur',
        },
    ],
};

const onSubmit = () => {
    loginFormRef.value.validate().then(async () => {
        const res = (await LoginAPI.setLogin(toRaw(loginForm))) as loginResConfig;
        if (res) {
            console.log(res);
            userStore.setToken(res.token);
            userStore.setUserInfo(res.userInfo);
            userStore.setRoutes(res.routes);
            addRoutes(userStore, router);
            router.push({ path: '/' });
        }
    });
};
</script>

<style lang="less" scoped>
......
</style>

八、页面刷新保持动态路由,退出登录清除动态路由

  1. 修改src/router/index.ts
// @/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router';
import { useUserStore } from '@/store';
import { addRoutes, clerRoutes } from '@/util/anyncRoutes';

const routes = [
    {
        path: '/',
        name: 'home',
        component: () => import('@/views/Home/index.vue'),
        meta: {
            title: '首页',
            layout: true,
        },
    },
    {
        path: '/login',
        name: 'Login',
        component: () => import('@/views/Login/index.vue'),
        meta: {
            title: '登录',
            layout: false,
        },
    },
];

const router = createRouter({
    history: createWebHashHistory(),
    routes,
});

let registerRouteFresh = true;
router.beforeEach((to, from, next) => {
    const userStore = useUserStore();

    if (to.name !== 'Login' && !userStore.token) {
        next({ name: 'Login' });
    } else {
        if (to.name === 'Login') {
            userStore.clearToken();
            userStore.clearUser();
            clerRoutes(userStore, router);
        }
        if (!from.name && registerRouteFresh) {
            addRoutes(userStore, router);
            next({ ...to, replace: true });
            registerRouteFresh = false;
        } else {
            next();
        }
    }
});

export default router;

九、源代码地址

https://github.com/jiangzetian/vue3-admin-template

十、视频演示及源码

本文演示视频:点击浏览

更多前端内容欢迎关注公众号:天小天个人网