后端使用 AI 开发前端速成:第七期:路由、权限与页面骨架
第七期:路由、权限与页面骨架
本期目标:掌握管理后台的"骨架"搭建——侧边栏、路由、登录、权限
核心理念:页面骨架是管理后台的"基础设施",和用户列表页一样重要
目录
- 第一章:管理后台骨架结构
- 第二章:Vue Router / React Router
- 第三章:登录页开发
- 第四章:路由守卫与权限控制
- 第五章:布局组件——侧边栏 + 顶部栏
- 第六章:Axios 封装实战
- 第七章:课后作业
第一章:管理后台骨架结构
1.1 典型骨架布局
┌─────────────────────────────────────┐ │ Logo 顶部导航栏(面包屑、用户) │ ← Header ├──────────┬──────────────────────────┤ │ │ │ │ 侧边栏 │ 内容区域(路由页面) │ ← Layout │ 菜单 │ │ │ │ │ │ │ │ ├──────────┴──────────────────────────┤ │ Footer │ └─────────────────────────────────────┘1.2 骨架必备元素
| 元素 | 用途 | 技术实现 |
|---|---|---|
| 侧边栏菜单 | 页面导航 | 根据路由配置生成 |
| 顶部栏 | 面包屑、用户信息、退出 | 独立组件 |
| 内容区 | 路由页面渲染 | <router-view>/<Outlet> |
| 路由系统 | URL 到页面的映射 | Vue Router / React Router |
| 路由守卫 | 登录拦截、权限校验 | beforeEach / loader |
| Axios 封装 | 统一请求处理 | 拦截器 |
第二章:Vue Router / React Router
2.1 Vue Router 4
// router/index.tsimport{createRouter,createWebHistory}from'vue-router'constroutes=[{path:'/login',component:()=>import('../views/Login.vue'),meta:{public:true}// 公开页面,不需要登录},{path:'/',component:()=>import('../layouts/MainLayout.vue'),redirect:'/dashboard',children:[{path:'dashboard',component:()=>import('../views/Dashboard.vue'),meta:{title:'首页',icon:'HomeFilled'}},{path:'users',component:()=>import('../views/UserList.vue'),meta:{title:'用户管理',icon:'UserFilled'}},{path:'users/:id/edit',component:()=>import('../views/UserEdit.vue'),meta:{title:'编辑用户',hidden:true}// 不在菜单显示}]},{path:'/:pathMatch(.*)*',redirect:'/404'}]constrouter=createRouter({history:createWebHistory(),routes})exportdefaultrouter2.2 React Router 6
// router/index.tsx import { createBrowserRouter, RouterProvider } from 'react-router-dom' const router = createBrowserRouter([ { path: '/login', element: <Login />, meta: { public: true } }, { path: '/', element: <MainLayout />, children: [ { path: '', element: <Navigate to="/dashboard" /> }, { path: 'dashboard', element: <Dashboard />, meta: { title: '首页', icon: 'HomeOutlined' } }, { path: 'users', element: <UserList />, meta: { title: '用户管理', icon: 'UserOutlined' } } ] }, { path: '*', element: <Navigate to="/404" /> } ]) export default router第三章:登录页开发
3.1 Vue 登录页
<template> <div class="login-page"> <el-card class="login-card"> <h2>管理后台</h2> <el-form ref="formRef" :model="form" :rules="rules" @keyup.enter="handleLogin" > <el-form-item prop="username"> <el-input v-model="form.username" placeholder="用户名" prefix-icon="User" /> </el-form-item> <el-form-item prop="password"> <el-input v-model="form.password" type="password" placeholder="密码" prefix-icon="Lock" show-password /> </el-form-item> <el-form-item> <el-button type="primary" :loading="loading" style="width: 100%" @click="handleLogin" > 登录 </el-button> </el-form-item> </el-form> </el-card> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' import request from '@/utils/request' const router = useRouter() const userStore = useUserStore() const loading = ref(false) const formRef = ref() const form = reactive({ username: '', password: '' }) const rules = { username: [{ required: true, message: '请输入用户名' }], password: [{ required: true, message: '请输入密码' }] } const handleLogin = async () => { await formRef.value?.validate() loading.value = true try { const { data } = await request.post('/api/login', form) userStore.setToken(data.token) userStore.setUserInfo(data.userInfo) ElMessage.success('登录成功') router.push('/') } catch (error) { ElMessage.error('登录失败') } finally { loading.value = false } } </script> <style scoped> .login-page { height: 100vh; display: flex; justify-content: center; align-items: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .login-card { width: 360px; } </style>第四章:路由守卫与权限控制
4.1 Vue 路由守卫
// router/index.tsimport{useUserStore}from'@/stores/user'// 路由守卫(类似后端拦截器)router.beforeEach((to,from,next)=>{constuserStore=useUserStore()consttoken=userStore.token// 不需要登录的页面直接放行if(to.meta?.public){next()return}// 未登录跳转登录页if(!token){next('/login')return}// 已登录但不能访问该页面(权限校验)constrequiredRoles=to.meta?.rolesasstring[]if(requiredRoles&&!requiredRoles.includes(userStore.userInfo?.role)){next('/403')return}next()})4.2 React 路由守卫
// 使用 Outlet + 条件渲染 import { Navigate, Outlet } from 'react-router-dom' import { useUserStore } from '@/stores/userStore' function AuthGuard() { const { isLoggedIn } = useUserStore() return isLoggedIn ? <Outlet /> : <Navigate to="/login" replace /> } function PermissionGuard({ roles }: { roles: string[] }) { const { userInfo } = useUserStore() const hasPermission = roles.includes(userInfo?.role) return hasPermission ? <Outlet /> : <Navigate to="/403" replace /> } // 路由配置 { path: '/', element: <AuthGuard />, // 先检查登录 children: [ { path: 'admin', element: <PermissionGuard roles={['admin']} />, // 再检查权限 children: [ { path: '', element: <AdminPage /> } ] } ] }第五章:布局组件——侧边栏 + 顶部栏
5.1 Vue 布局组件
<template> <el-container class="layout"> <!-- 侧边栏 --> <el-aside width="200px" class="sidebar"> <div class="logo">管理后台</div> <el-menu :default-active="activeMenu" router background-color="#001529" text-color="#fff" active-text-color="#409EFF" > <el-menu-item index="/dashboard"> <el-icon><HomeFilled /></el-icon> <span>首页</span> </el-menu-item> <el-menu-item index="/users"> <el-icon><UserFilled /></el-icon> <span>用户管理</span> </el-menu-item> </el-menu> </el-aside> <el-container> <!-- 顶部栏 --> <el-header class="header"> <breadcrumb /> <div class="user-info"> <span>{{ userStore.userName }}</span> <el-button type="text" @click="handleLogout">退出</el-button> </div> </el-header> <!-- 内容区 --> <el-main class="main"> <router-view /> </el-main> </el-container> </el-container> </template> <script setup> import { computed } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useUserStore } from '@/stores/user' const route = useRoute() const router = useRouter() const userStore = useUserStore() const activeMenu = computed(() => route.path) const handleLogout = () => { userStore.logout() router.push('/login') } </script>5.2 动态菜单
// 根据路由配置生成菜单constmenuRoutes=router.getRoutes().filter(r=>r.meta?.title&&!r.meta?.hidden).map(r=>({path:r.path,title:r.meta.title,icon:r.meta.icon}))第六章:Axios 封装实战
6.1 请求封装
// utils/request.tsimportaxiosfrom'axios'import{ElMessage}from'element-plus'import{useUserStore}from'@/stores/user'importrouterfrom'@/router'constrequest=axios.create({baseURL:import.meta.env.VITE_API_BASE_URL||'/api',timeout:10000})// 请求拦截器request.interceptors.request.use((config)=>{consttoken=useUserStore().tokenif(token){config.headers.Authorization=`Bearer${token}`}returnconfig},(error)=>Promise.reject(error))// 响应拦截器request.interceptors.response.use((response)=>{const{code,data,message}=response.dataif(code===200){returndata}ElMessage.error(message||'请求失败')returnPromise.reject(newError(message))},(error)=>{if(error.response?.status===401){useUserStore().logout()router.push('/login')ElMessage.error('登录已过期')}else{ElMessage.error(error.response?.data?.message||'网络错误')}returnPromise.reject(error)})exportdefaultrequest6.2 React 版本
// utils/request.tsimportaxiosfrom'axios'import{message}from'antd'import{useUserStore}from'@/stores/userStore'constrequest=axios.create({baseURL:import.meta.env.VITE_API_BASE_URL,timeout:10000})request.interceptors.request.use((config)=>{consttoken=useUserStore.getState().tokenif(token){config.headers.Authorization=`Bearer${token}`}returnconfig})request.interceptors.response.use((response)=>{const{code,data,message:msg}=response.dataif(code===200)returndata message.error(msg||'请求失败')returnPromise.reject(newError(msg))},(error)=>{if(error.response?.status===401){useUserStore.getState().logout()window.location.href='/login'message.error('登录已过期')}else{message.error(error.response?.data?.message||'网络错误')}returnPromise.reject(error)})exportdefaultrequest第七章:实战
7.1 必做实战
实战 1:搭建完整的管理后台骨架
创建一个包含以下功能的骨架项目:
- 登录页(用户名 + 密码)
- 路由系统(首页、用户管理、404)
- 路由守卫(未登录拦截)
- 布局组件(侧边栏 + 顶部栏)
- Axios 封装(自动加 Token + 401 处理)
实战 2:添加权限控制
在骨架基础上:
- 增加角色字段(admin / user)
- 某些菜单只有 admin 能看到
- 某些操作按钮按角色显示/隐藏
7.2 FAQ
Q:路由守卫和后端拦截器有什么区别?
路由守卫是前端的"客户端校验",可以被绕过(直接访问 URL)。后端拦截器才是真正的安全防线。
Q:Token 过期了怎么处理?
在 Axios 响应拦截器中捕获 401,清除本地 Token,跳转到登录页。不需要刷新 Token 的复杂逻辑(简单的管理后台)。
下一期预告:对接真实后端接口 —— 打通前后端联调全流程
