AI 摘要

文章聚焦 Solon + EasyQuery + ElementPlus 后台管理系统的体验优化:通过 Axios 请求/响应拦截器统一注入 Token、集中处理 401 及错误提示,消除冗余代码;借助 Vue-Router 全局前置守卫,按 Token 有无精准拦截或放行路由,杜绝未登录越权访问;利用嵌套路由抽取 Layout,将侧边栏、顶栏、底栏固化为父组件,子页仅渲染核心内容,并引入 ElementPlus ICON 与统一样式,显著精简组件、提升可维护性与视觉一致性。

Axios 优化

Axios 存在的问题

由于后端接口大部分需要登录状态下才能请求访问,前后端验证登录状态,凭借的是在请求时请求头中传递的 Token 令牌,也就是说,每一次请求后端接口,都要在请求头中传递 Token 令牌。

// 发送请求,获取登录用户信息实体
axios.get('/passport/currentUser', {
    // 发送请求时,添加请求头Header,传递Token
    headers: {
        'authtoken': localStorage.getItem('authtoken')
    }
}).then((res) => {
    if (res.data.code === 200) {
        // 将登录用户信息实体存储到数据仓库中
        useCurrentUserStore().setCurrentUser(res.data.data)
    } else {
        ElMessage.error(res.data.message)
    }
}).catch((error) => {
    ElMessage.error(error.message)
})

再者,Axios 发完请求之后,要对响应状态做判断,如果响应状态不是 200,需要提示错误信息。

综上所述:

  • 前端大量 Axios 请求后端,需要携带 Token。
  • 后端响应数据给前端时,前端需要进行状态判断。
  • 以上工作,几乎是每一个 Axios 请求必备的,如果在每一次 Axios 请求时都手动操作处理,会出现严重的冗余问题。

Axios 优化

核心注意事项:

  • 在 Axios 请求拦截器中,从 localStorage 中获取 Token 令牌,并根据实际情况存到请求头中。
  • 在 Axios 响应拦截器中,直接对响应状态进行判断,最后直接返回响应数据,简化接口调用处的取值逻辑。

编写 Axios 配置文件(plugins/axios.js),优化请求拦截器和响应拦截器。

// Axios请求拦截器
_axios.interceptors.request.use(
    function(config) {
        // 从localStorage中取出Token,如果Token存在,则放入请求头
        let authtoken = localStorage.getItem("authtoken");
        if (authtoken) {
            config.headers.authtoken = authtoken;
        }

        return config;
    },
    function(error) {
        // Do something with request error
        return Promise.reject(error);
    }
);

// Axios响应拦截器
_axios.interceptors.response.use(
    function(response) {
        if(response.data.code === 401) {
            // 如果响应数据中code为401即未登录,则清空locaoStorage跳转到登录页面
            // 可能:未登录、Token 失效
            localStorage.clear()
            ElMessage.error("请先登录")
            router.push("/login")
            // 抛出错误
            return Promise.reject(response.data.message)
        } else if(response.data.code !== 200) {
            // 响应数据中code不为200,则提示错误信息
            ElMessage.error(response.data.message)
            // 抛出错误
            return Promise.reject(response.data.message)
        }

        // 直接返回响应数据,简化接口调用处的取值逻辑
        return response.data;
    },
    function(error) {
        // 处理响应失败(HTTP状态码非2xx)
        ElMessage.error(error.message)
        return Promise.reject(error);
    }
);

编写登录组件(views/Login.vue),优化 Axios 请求:

// 这里执行实际的登录逻辑
// 发送Axios请求
axios.post('/passport/login', qs.stringify(loginForm)).then((res) => {
    ElMessage.success('登录成功')
    // 将Token存储到localStorage中
    // Axios响应拦截器处理,res即后端响应给前端的数据,不再是res.data.data
    localStorage.setItem('authtoken', res.data)
    // 跳转到后台首页
    router.push('/admin/index')
})

编写后台首页组件(views/Index.vue),优化 Axios 请求:

// 发送请求,获取登录用户信息实体
axios.get('/passport/currentUser').then((res) => {
    useCurrentUserStore().setCurrentUser(res.data)
})

路由守卫

登录状态认证存在的问题

在未登录的情况下,可以直接访问到后台首页视图,不符合业务逻辑。虽然在后台首页组件加载时,会发送 Axios 请求到后端获取登录用户信息实体,未登录的情况下,会在 Axios 响应拦截器中对 401 状态进行处理,但本质上是不能在未登录的情况下触发 Axios 请求的。

登录状态认证优化

核心注意事项:

  • 通过 Vue-Router 提供的路由守卫,进行前端路由拦截。
  • 如果 Token 存在且访问了登录路由,直接跳转到后台首页。
  • 如果 Token 不存在且访问了登录路由,直接跳过,不做额外处理。
  • 如果 Token 不存在且访问了后台其他路由,直接跳转到登录页面。
  • Token 存在且访问了后台其他路由,直接跳过,不做额外处理。

编写路由配置文件(router/index.js),完善路由守卫:

router.beforeEach((to, from, next) => {
    let authtoken = localStorage.getItem("authtoken")
    // 通过to.path获取访问的路由地址,并进行判断
    if(to.path === '/login' && authtoken) {
        // 如果Token存在且访问了登录路由,跳转到首页页面
        ElMessage.success('欢迎回来')
        next({path: '/admin/index'})
    } else if(to.path === '/login') {
        // 跳过登录路由
        next()
    } else if(!authtoken) {
        // 如果Token不存在,即未登录,跳转到登录页面
        ElMessage.error('请先登录')
        next({path: '/login'})
    }

    // 其他正常情况,继续访问
    next()
})

视图组件优化

抽取公共布局

后台页面,事实上只有核心区域 main 会发生变化,侧边栏、顶部导航栏、底部版权声明栏是不需要变化的。

核心注意事项:

  • 可以通过嵌套路由抽取公共布局,也可以通过组件的形式抽取公共布局。
  • 通过嵌套路由抽取公共布局时,父路由的 component 必须是布局组件,其核心区域使用 router-view 出口,用于承载子路由内容,子路由的 component 对应具体的页面组件,会被渲染到父路由的 router-view 中。
  • 抽取后公共布局后,子路由的对应的业务组件只需写核心内容(如表格、表单),公共由父路由的对应的布局组件自动承载。

后台首页只有核心区域变化而已,其他区域都不变。

重命名后台首页组件(views/Index.vue)为布局组件(views/Layouts.vue),并修改主要内容区:

<!-- 主要内容区 -->
<router-view/>

编写后台首页组件(views/Index.vue),提供后台首页视图:

<script setup>
import {useCurrentUserStore} from "@/stores/currentUser.js";
</script>

<template>
    <el-main class="layout-main">
        <div class="welcome-container">
            <h2>欢迎您,{{ useCurrentUserStore().currentUser.realname }}!</h2>
            <p>祝您工作愉快!</p>
        </div>
    </el-main>
</template>

编写路由配置文件(router/index.js),提供嵌套路由信息:

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        {
            // 根路径跳转到登录路由
            path: '',
            redirect: '/login'
        },
        {
            // 登录
            path: '/login',
            component: () => import('@/views/Login.vue'),
        },
        {
            // 后台
            path: '/admin',
            component: () => import('@/views/Layouts.vue'),
            // 使用嵌套路由
            // 嵌套路由的component会渲染到父路由的router-view中
            children: [
                {
                    // 后台首页
                    path: 'index',    // 子路由路径前不能加/号,访问路径为父路由路径+/子路由路径
                    component: () => import('@/views/Index.vue'),
                }
            ]
        },
    ],
})

ElementPlus ICON

核心注意事项:

在终端中执行指令,安装 ElementPlus ICON:

npm install @element-plus/icons-vue

编写项目入口文件(main.js),批量注册 ElementPlus ICON:

// 引入ElementPlus图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)

// 批量注册ElementPlus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
}

登录组件样式优化

编写登录组件(views/Login.vue),优化样式:

<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import router from "@/router/index.js";
import qs from "qs";

// 表单数据
const loginForm = reactive({
    username: '',
    password: ''
})

// 表单引用
const loginFormRef = ref()

// 表单验证规则
const loginRules = {
    username: [
        { required: true, message: '请输入账户账号', trigger: 'blur' },
        { min: 6, max: 18, message: '账户账号长度应为6-18个字符', trigger: 'blur' }
    ],
    password: [
        { required: true, message: '请输入账户密码', trigger: 'blur' },
        { min: 6, max: 18, message: '账户密码长度应为6-18个字符', trigger: 'blur' }
    ]
}

// 登录处理函数
const handleLogin = async () => {
    if (!loginFormRef.value) return

    await loginFormRef.value.validate((valid) => {
        if (valid) {
            // 这里执行实际的登录逻辑
            // 发送Axios请求
            axios.post('/passport/login', qs.stringify(loginForm)).then((res) => {
                ElMessage.success('登录成功')
                // 将Token存储到localStorage中
                // Axios响应拦截器处理,res即后端响应给前端的数据,不再是res.data.data
                localStorage.setItem('authtoken', res.data)
                // 跳转到后台首页
                router.push('/admin/index')
            })
        } else {
            ElMessage.error('请正确填写表单信息')
            return false
        }
    })
}

// 重置表单
const resetForm = () => {
    loginFormRef.value.resetFields()
}
</script>

<template>
    <el-container>
        <el-main>
            <el-row justify="center">
                <el-col :span="8">
                    <el-card style="margin-top: 5rem">
                        <template #header>
                            <div style="text-align: center; padding: 0.5rem 0">
                                <h2 style="margin-top: 0; margin-bottom: 10px">SAdmin后台管理系统</h2>
                                <span>欢迎使用SAdmin后台管理系统,请先登录</span>
                            </div>
                        </template>

                        <el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" label-width="80px" label-position="top">
                            <el-form-item label="账户账号" prop="username" size="large">
                                <el-input v-model="loginForm.username" placeholder="请输入账户账号"/>
                            </el-form-item>

                            <el-form-item label="账户密码" prop="password" size="large">
                                <el-input v-model="loginForm.password" type="password" placeholder="请输入账户密码" show-password/>
                            </el-form-item>

                            <div style="width: 100%; text-align: center">
                                <el-button type="primary" size="large" @click="handleLogin">登录</el-button>
                                <el-button size="large" @click="resetForm">重置</el-button>
                            </div>
                        </el-form>
                    </el-card>
                </el-col>
            </el-row>
        </el-main>
    </el-container>
</template>

在浏览器中测试:

根组件样式优化

编写根组件(App.vue),优化样式:

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

<style>
/** 去除body外边距 **/
body {
    margin: 0;
}
</style>

布局组件样式优化

编写布局组件(views/Layouts.vue),优化样式:

<script setup>
import { ref } from 'vue'
import {useCurrentUserStore} from "@/stores/currentUser.js";
import router from "@/router/index.js";

// 模拟菜单数据
const menuData = ref([
    {
        id: 1,
        title: '系统管理',
        children: [
            {
                id: 11,
                title: '用户管理',
                path: '/users'
            },
            {
                id: 12,
                title: '角色管理',
                path: '/role'
            },
            {
                id: 13,
                title: '部门管理',
                path: '/dept'
            },
            {
                id: 20,
                title: '权限管理',
                path: '/perms'
            }
        ]
    },
    {
        id: 2,
        title: '日志管理',
        children: [
            {
                id: 21,
                title: '登录日志',
                path: '/loginlog'
            },
            {
                id: 22,
                title: '操作日志',
                path: '/actionlog'
            }
        ]
    }
])

// 发送请求,获取登录用户信息实体
axios.get('/passport/currentUser').then((res) => {
    // Axios响应拦截器处理,res即后端响应给前端的数据
    useCurrentUserStore().setCurrentUser(res.data)
})

// 注销登录
const logout = () => {
    axios.post('/passport/logout').then((res) => {
        // 登出成功,清空locaoStorage跳转到登录页面
        localStorage.clear()
        router.push('/login')
    })
}
</script>

<template>
    <el-container class="layout-container">
        <!-- 左侧菜单栏 -->
        <el-aside width="200px">
            <!--
                router="true":启用该模式会在激活导航时以index作为path进行路由跳转
                参考文档:https://element-plus.org/zh-CN/component/menu#menu-attributes
            -->
            <el-menu default-active="1" class="el-menu-vertical" :router="true" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b">
                <el-menu-item index="/admin/index">
                    <span>SAdmin后台管理系统</span>
                </el-menu-item>

                <template v-for="item in menuData" :key="item.id">
                    <el-sub-menu v-if="item.children && item.children.length > 0" :index="item.id">
                        <template #title>
                            <span>{{ item.title }}</span>
                        </template>
                        <!--
                            index注意路径拼接前缀/admin
                        -->
                        <el-menu-item v-for="child in item.children" :key="child.id" :index="'/admin' + child.path">
                            {{ child.title }}
                        </el-menu-item>
                    </el-sub-menu>

                    <el-menu-item v-else :index="'/admin' + item.path">
                        <template #title>{{ item.title }}</template>
                    </el-menu-item>
                </template>
            </el-menu>
        </el-aside>

        <!-- 右侧内容区 -->
        <el-container>
            <!-- 顶部导航栏 -->
            <el-header class="layout-header">
                <div class="header-right">
                    <el-dropdown>
                        <div class="el-dropdown-link">
                            欢迎您,{{ useCurrentUserStore().currentUser.realname }}
                            <el-icon class="el-icon--right">
                                <arrow-down />
                            </el-icon>
                        </div>
                        <template #dropdown>
                            <el-dropdown-menu>
                                <el-dropdown-item>修改信息</el-dropdown-item>
                                <el-dropdown-item>修改密码</el-dropdown-item>
                                <el-dropdown-item>登录日志</el-dropdown-item>
                                <el-dropdown-item>操作日志</el-dropdown-item>
                                <el-dropdown-item @click="logout">注销</el-dropdown-item>
                            </el-dropdown-menu>
                        </template>
                    </el-dropdown>
                </div>
            </el-header>

            <!-- 主要内容区 -->
            <router-view/>

            <!-- 底部版权信息 -->
            <el-footer class="layout-footer">
                <div class="footer-content">
                    <p>© 2025 后台管理系统. All Rights Reserved.</p>
                </div>
            </el-footer>
        </el-container>
    </el-container>
</template>

<style>
.layout-container {
    height: 100vh;
}

.el-aside {
    background-color: #545c64;
}

.el-menu-vertical {
    height: 100%;
    border: none;
}

.layout-header {
    background-color: #ffffff;
    color: #333;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding: 0 20px;
}

.header-right {
    display: flex;
    align-items: center;
}

.el-dropdown-link {
    cursor: pointer;
    color: #333;
    display: flex;
    align-items: center;
    outline: none !important;
    box-shadow: none !important;
}

.layout-main {
    background-color: #f5f7fa;
    padding: 20px;
}

.layout-footer {
    background-color: #ffffff;
    color: #666;
    text-align: center;
    padding: 20px;
    border-top: 1px solid #ebeef5;
}

.footer-content p {
    margin: 0;
}
</style>

后台首页组件样式优化

编写后台首页组件(views/Index.vue),优化样式:

<script setup>
import {useCurrentUserStore} from "@/stores/currentUser.js";
</script>

<template>
    <el-main class="layout-main">
        <el-card>
            <template #header>
                <div class="card-header">
                    <span>后台首页</span>
                </div>
            </template>
            欢迎您,{{useCurrentUserStore().currentUser.realname}},祝您工作愉快!
        </el-card>
    </el-main>
</template>

在浏览器中测试: