
前言
后台管理系统是前端开发中最常见的业务场景之一,也是 Vue 生态工具整合应用的典型案例。很多新手在学习 Vue3 时,往往只会单独使用某个工具(比如只写路由、只做状态管理),但到了实际项目中,如何把 Vue Router、Pinia、Axios、Element Plus 这些核心工具串联起来,实现一个完整的后台系统,却常常无从下手。
本文将从实战落地角度,手把手教你从 0 搭建一个具备完整功能的后台管理系统核心模块:涵盖多页面路由配置与导航 、Pinia 用户状态持久化 、Axios 企业级请求封装 、Element Plus 登录页与首页开发。内容覆盖基础配置、核心逻辑、踩坑解决方案,既适合 Vue3 新手入门实战,也能为中后台项目开发提供可落地的参考方案。
一、项目初始化与技术栈选型
1.1 核心技术栈说明
本次实战项目采用 Vue3 生态最主流、最稳定的技术组合,适配企业级开发标准:
| 技术工具 | 核心作用 | 选择理由 |
|---|---|---|
| Vue3 + Vite | 基础框架 / 构建工具 | Vue3 组合式 API 开发效率高,Vite 打包速度比 Webpack 快 3-5 倍 |
| Vue Router 4 | 路由管理 | Vue3 官方配套路由,支持组合式 API、路由守卫 |
| Pinia 2 | 状态管理 | 替代 Vuex,更轻量、支持 TS、无需嵌套模块 |
| Axios | 网络请求 | 支持拦截器、取消请求、跨域处理,后台系统请求必备 |
| Element Plus | UI 组件库 | 企业级后台组件丰富,适配 Vue3,文档友好 |
| TypeScript | 类型校验 | 提升代码可维护性,减少生产环境 bug |
1.2 环境准备
确保本地已安装以下环境:
- Node.js ≥ 14.18.0(推荐 16+/18+)
- npm/pnpm/yarn(推荐 pnpm,速度更快、体积更小)
- VS Code(安装 Vetur/Vue Official 扩展)
1.3 项目创建与依赖安装
步骤 1:创建 Vue3+TS+Vite 项目
bash
# 创建项目(命名为vue-admin-demo)
pnpm create vite vue-admin-demo --template vue-ts
# 进入项目目录
cd vue-admin-demo
# 安装基础依赖
pnpm install
步骤 2:安装核心依赖
bash
# 路由
pnpm add vue-router@4
# 状态管理
pnpm add pinia pinia-plugin-persistedstate
# 网络请求
pnpm add axios
# UI组件库
pnpm add element-plus
# Element Plus按需引入插件
pnpm add unplugin-vue-components unplugin-auto-import -D
1.4 项目目录结构梳理
初始化后整理项目结构(符合后台系统开发规范):
bash
vue-admin-demo/
├── src/
│ ├── api/ # Axios封装与接口请求
│ ├── assets/ # 静态资源(图片、样式)
│ ├── components/ # 通用组件
│ ├── layouts/ # 布局组件(后台首页布局)
│ ├── pinia/ # Pinia状态管理
│ ├── router/ # 路由配置
│ ├── styles/ # 全局样式
│ ├── utils/ # 工具函数
│ ├── views/ # 页面视图(登录、首页、列表、详情)
│ ├── App.vue # 根组件
│ ├── main.ts # 入口文件
│ └── vite-env.d.ts # TS类型声明
├── .env.development # 开发环境变量
├── vite.config.ts # Vite配置
└── package.json # 依赖配置
二、Vue Router 多页面应用开发
2.1 路由核心逻辑梳理
后台系统的路由核心需求:
- 区分无需登录可访问(登录页)和需登录访问(首页、列表页、详情页)
- 支持页面间导航(导航菜单、编程式跳转)
- 详情页支持参数传递(如商品 ID、用户 ID)
- 全局路由守卫校验登录状态
用 Mermaid 可视化路由结构:

2.2 路由配置实现
步骤 1:创建路由配置文件(src/router/index.ts)
javascript
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { useUserStore } from '@/pinia/user'; // 后续Pinia会实现
// 定义路由规则
const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/home', // 默认跳转到首页
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login/index.vue'), // 懒加载
meta: {
requiresAuth: false, // 无需登录
title: '登录页',
},
},
{
path: '/home',
name: 'Home',
component: () => import('@/views/Home/index.vue'),
meta: {
requiresAuth: true, // 需要登录
title: '后台首页',
},
},
{
path: '/list',
name: 'List',
component: () => import('@/views/List/index.vue'),
meta: {
requiresAuth: true,
title: '数据列表页',
},
},
{
path: '/detail/:id', // 动态参数id
name: 'Detail',
component: () => import('@/views/Detail/index.vue'),
meta: {
requiresAuth: true,
title: '详情页',
},
},
// 404页面
{
path: '/:pathMatch(.*)*',
redirect: '/home',
},
];
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
// 全局路由守卫:校验登录状态
router.beforeEach((to, from, next) => {
// 设置页面标题
document.title = to.meta.title as string || 'Vue后台管理系统';
const userStore = useUserStore();
// 判断当前路由是否需要登录
if (to.meta.requiresAuth) {
if (userStore.token) {
// 已登录,正常访问
next();
} else {
// 未登录,跳转到登录页
next({ path: '/login' });
}
} else {
// 无需登录,直接访问
next();
}
});
export default router;
步骤 2:在入口文件注册路由(src/main.ts)
javascript
import { createApp } from 'vue';
import App from './App.vue';
// 引入路由
import router from './router';
// 引入Pinia(后续配置)
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
// 引入Element Plus(后续配置)
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
// 创建Pinia实例
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate); // 持久化插件
const app = createApp(App);
// 注册插件
app.use(router);
app.use(pinia);
app.use(ElementPlus);
app.mount('#app');
步骤 3:修改根组件(src/App.vue)
html
<template>
<!-- 路由出口:所有页面都会渲染到这里 -->
<router-view />
</template>
<script setup lang="ts">
// 根组件仅作为路由容器
</script>
<style scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Microsoft YaHei", sans-serif;
}
</style>
2.3 路由导航与参数传递
1. 声明式导航(导航菜单 / 按钮)
在首页(Home)添加导航按钮,跳转到列表页 / 详情页:
html
<template>
<div class="home">
<h2>后台首页</h2>
<!-- 跳转到列表页 -->
<el-button type="primary" @click="$router.push('/list')">
前往数据列表页
</el-button>
<!-- 跳转到详情页(传递参数id=1) -->
<el-button type="success" @click="$router.push('/detail/1')">
查看ID=1的详情
</el-button>
<!-- 编程式导航的另一种写法 -->
<el-button type="warning" @click="toDetail(2)">
查看ID=2的详情
</el-button>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
// 编程式导航传递参数
const toDetail = (id: number) => {
router.push({
path: `/detail/${id}`,
// 也可以用query传递参数:
// query: { id: id }
});
};
</script>
2. 详情页接收参数(src/views/Detail/index.vue)
html
<template>
<div class="detail">
<h2>详情页</h2>
<p>当前详情ID:{{ id }}</p>
<!-- 如果用query传递,取值:$route.query.id -->
<el-button @click="$router.go(-1)">返回上一页</el-button>
</div>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';
const route = useRoute();
// 获取params参数(注意:需添加TS类型断言)
const id = route.params.id as string;
</script>
三、Pinia 用户状态管理(登录状态 + 持久化)
3.1 Pinia 核心优势(对比 Vuex)

3.2 定义用户 Store(src/pinia/user.ts)
javascript
import { defineStore } from 'pinia';
// 定义用户信息类型
interface UserInfo {
id: number;
username: string;
avatar: string;
roles: string[];
}
// 定义Store状态类型
interface UserState {
token: string; // 登录令牌
userInfo: UserInfo | null; // 用户信息
isLogin: boolean; // 是否登录
}
// 定义Store
export const useUserStore = defineStore('user', {
// 状态初始化
state: (): UserState => ({
token: '',
userInfo: null,
isLogin: false,
}),
// 计算属性(类似Vuex的getters)
getters: {
// 获取用户角色(简化写法)
userRoles: (state) => state.userInfo?.roles || [],
},
// 方法(同步/异步,类似Vuex的mutations+actions)
actions: {
// 登录:存储token和用户信息
login(token: string, userInfo: UserInfo) {
this.token = token;
this.userInfo = userInfo;
this.isLogin = true;
},
// 退出登录:清空状态
logout() {
this.token = '';
this.userInfo = null;
this.isLogin = false;
// 退出后跳转到登录页
window.location.href = '/login';
},
// 更新用户信息
updateUserInfo(userInfo: Partial<UserInfo>) {
if (this.userInfo) {
this.userInfo = { ...this.userInfo, ...userInfo };
}
},
},
// 持久化配置(关键!)
persist: {
enabled: true, // 开启持久化
strategies: [
{
key: 'vue_admin_user', // 本地存储的key
storage: localStorage, // 存储方式:localStorage/sessionStorage
// 指定需要持久化的字段(按需选择)
paths: ['token', 'userInfo', 'isLogin'],
},
],
},
});
3.3 Pinia 实战使用(登录 / 退出)
1. 登录时存储状态(后续登录页会调用)
javascript
import { useUserStore } from '@/pinia/user';
const userStore = useUserStore();
// 模拟登录接口返回的token和用户信息
const mockToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const mockUserInfo = {
id: 1,
username: 'admin',
avatar: 'https://avatars.githubusercontent.com/u/123456',
roles: ['admin'],
};
// 调用Store的login方法
userStore.login(mockToken, mockUserInfo);
2. 退出登录(首页导航栏使用)
html
<template>
<el-button type="danger" @click="handleLogout">退出登录</el-button>
</template>
<script setup lang="ts">
import { useUserStore } from '@/pinia/user';
const userStore = useUserStore();
// 退出登录
const handleLogout = () => {
userStore.logout();
};
</script>
四、Axios 封装(后台系统专属版)
4.1 封装核心思路
后台系统的 Axios 封装需满足:
- 统一基础 URL(区分开发 / 生产环境)
- 请求拦截器:自动添加 token、设置请求头
- 响应拦截器:统一错误处理、token 过期刷新
- 加载状态提示(Element Plus Loading)
- 封装通用请求方法(get/post/put/delete)
4.2 完整封装实现(src/api/request.ts)
javascript
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus';
import { useUserStore } from '@/pinia/user';
// 定义接口返回数据类型(适配后台接口规范)
interface ResponseData<T = any> {
code: number; // 业务状态码(200成功,401token过期,500服务器错误)
msg: string; // 提示信息
data: T; // 业务数据
}
// 创建Axios实例
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量中的接口地址
timeout: 10000, // 超时时间
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
});
// 加载状态实例
let loadingInstance: ReturnType<typeof ElLoading.service> | null = null;
// 请求拦截器
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 开启加载提示
loadingInstance = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.5)',
});
// 添加token到请求头
const userStore = useUserStore();
if (userStore.token && config.headers) {
config.headers.Authorization = `Bearer ${userStore.token}`;
}
return config;
},
(error) => {
// 关闭加载提示
if (loadingInstance) loadingInstance.close();
ElMessage.error('请求发送失败:' + error.message);
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse<ResponseData>) => {
// 关闭加载提示
if (loadingInstance) loadingInstance.close();
const { code, msg, data } = response.data;
// 业务状态码处理
if (code === 200) {
return data; // 成功:直接返回业务数据
} else {
// 业务错误提示
ElMessage.error(msg || '请求失败');
return Promise.reject(new Error(msg || '请求失败'));
}
},
(error) => {
// 关闭加载提示
if (loadingInstance) loadingInstance.close();
// 网络错误/HTTP状态码处理
let errorMsg = '请求失败';
if (error.response) {
// HTTP状态码处理
switch (error.response.status) {
case 401:
// Token过期:提示并退出登录
ElMessageBox.confirm(
'登录状态已过期,请重新登录',
'提示',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
const userStore = useUserStore();
userStore.logout();
});
errorMsg = '登录状态已过期';
break;
case 403:
errorMsg = '暂无权限访问该资源';
break;
case 404:
errorMsg = '请求地址不存在';
break;
case 500:
errorMsg = '服务器内部错误,请稍后重试';
break;
default:
errorMsg = error.response.data?.msg || '请求失败';
}
} else if (error.message.includes('timeout')) {
errorMsg = '请求超时,请检查网络';
} else if (error.message.includes('Network Error')) {
errorMsg = '网络异常,请检查网络连接';
}
ElMessage.error(errorMsg);
return Promise.reject(new Error(errorMsg));
}
);
// 封装通用请求方法
export const request = {
// GET请求
get<T = any>(url: string, params?: any, config?: AxiosRequestConfig): Promise<T> {
return service.get(url, { params, ...config });
},
// POST请求
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return service.post(url, data, config);
},
// PUT请求
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return service.put(url, data, config);
},
// DELETE请求
delete<T = any>(url: string, params?: any, config?: AxiosRequestConfig): Promise<T> {
return service.delete(url, { params, ...config });
},
};
export default service;
4.3 配置环境变量(.env.development)
bash
# 开发环境接口地址(根据实际后端地址修改)
VITE_API_BASE_URL = 'http://localhost:3000/api'
# 项目基础路径
VITE_BASE_URL = '/'
4.4 封装业务接口(src/api/user.ts)
javascript
import { request } from './request';
// 定义登录接口参数类型
export interface LoginParams {
username: string;
password: string;
}
// 定义用户信息返回类型
export interface LoginResponse {
token: string;
userInfo: {
id: number;
username: string;
avatar: string;
roles: string[];
};
}
// 登录接口
export const loginApi = (params: LoginParams) => {
return request.post<LoginResponse>('/user/login', params);
};
// 获取用户信息接口
export const getUserInfoApi = () => {
return request.get<LoginResponse['userInfo']>('/user/info');
};
五、Element Plus 开发后台核心页面
5.1 Element Plus 按需引入配置(vite.config.ts)
为减少打包体积,配置按需引入:
javascript
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
// Element Plus按需引入插件
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
// 自动导入Element Plus API
AutoImport({
resolvers: [ElementPlusResolver()],
}),
// 自动导入Element Plus组件
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
// 配置@别名
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
// 开发服务器配置(跨域代理)
server: {
port: 3001, // 前端端口
proxy: {
// 代理接口请求,解决跨域
'/api': {
target: 'http://localhost:3000', // 后端接口地址
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
5.2 登录页开发(src/views/Login/index.vue)
包含表单验证、登录逻辑、整合 Pinia 和 Axios:
javascript
<template>
<div class="login-container">
<div class="login-card">
<h2 class="login-title">后台管理系统登录</h2>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
label-width="80px"
class="login-form"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
prefix-icon="el-icon-user"
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
prefix-icon="el-icon-lock"
/>
</el-form-item>
<el-form-item label="验证码" prop="code">
<el-input
v-model="loginForm.code"
placeholder="请输入验证码"
style="width: 60%; display: inline-block"
/>
<div class="code-img">
<!-- 验证码图片(模拟) -->
<img src="https://picsum.photos/100/40" alt="验证码" @click="refreshCode" />
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
class="login-btn"
@click="handleLogin"
:loading="loading"
>
登录
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import type { FormInstance, FormRules } from 'element-plus';
import { ElMessage } from 'element-plus';
import { useUserStore } from '@/pinia/user';
import { loginApi, type LoginParams } from '@/api/user';
// 表单Ref
const loginFormRef = ref<FormInstance>();
// 加载状态
const loading = ref(false);
// 路由实例
const router = useRouter();
// 用户Store
const userStore = useUserStore();
// 登录表单数据
const loginForm = ref<LoginParams & { code: string }>({
username: '',
password: '',
code: '',
});
// 表单验证规则
const loginRules = ref<FormRules>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度为3-20位', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度为6-20位', trigger: 'blur' },
],
code: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ len: 4, message: '验证码长度为4位', trigger: 'blur' },
],
});
// 刷新验证码
const refreshCode = () => {
// 模拟刷新验证码(修改图片URL)
const codeImg = document.querySelector('.code-img img') as HTMLImageElement;
codeImg.src = `https://picsum.photos/100/40?${Date.now()}`;
};
// 登录逻辑
const handleLogin = async () => {
if (!loginFormRef.value) return;
try {
// 表单验证
await loginFormRef.value.validate();
loading.value = true;
// 调用登录接口
const res = await loginApi({
username: loginForm.value.username,
password: loginForm.value.password,
});
// 存储登录状态到Pinia
userStore.login(res.token, res.userInfo);
// 登录成功提示
ElMessage.success('登录成功!');
// 跳转到首页
router.push('/home');
} catch (error) {
console.error('登录失败:', error);
ElMessage.error('登录失败,请检查账号密码');
} finally {
loading.value = false;
}
};
</script>
<style scoped lang="scss">
.login-container {
width: 100vw;
height: 100vh;
background: linear-gradient(120deg, #409eff, #67c23a);
display: flex;
justify-content: center;
align-items: center;
}
.login-card {
width: 400px;
padding: 30px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
.login-title {
text-align: center;
color: #409eff;
margin-bottom: 20px;
font-size: 20px;
}
.login-form {
margin-top: 20px;
}
.code-img {
display: inline-block;
margin-left: 10px;
img {
cursor: pointer;
border-radius: 4px;
}
}
.login-btn {
width: 100%;
height: 40px;
font-size: 16px;
}
</style>
5.3 后台首页开发(布局 + 导航菜单)
步骤 1:创建布局组件(src/layouts/MainLayout.vue)
html
<template>
<el-container style="height: 100vh;">
<!-- 侧边栏 -->
<el-aside width="200px" style="background-color: #2e3b4e;">
<div class="logo">
<img src="https://picsum.photos/40/40" alt="logo" />
<span>后台管理系统</span>
</div>
<!-- 导航菜单 -->
<el-menu
default-active="1"
class="el-menu-vertical-demo"
background-color="#2e3b4e"
text-color="#fff"
active-text-color="#ffd04b"
@select="handleMenuSelect"
>
<el-menu-item index="1">
<el-icon><House /></el-icon>
<template #title>首页</template>
</el-menu-item>
<el-menu-item index="2">
<el-icon><List /></el-icon>
<template #title>数据列表</template>
</el-menu-item>
<el-sub-menu index="3">
<template #title>
<el-icon><Setting /></el-icon>
<span>系统设置</span>
</template>
<el-menu-item index="3-1">用户管理</el-menu-item>
<el-menu-item index="3-2">角色管理</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<!-- 主内容区 -->
<el-container>
<!-- 顶部导航 -->
<el-header style="text-align: right; font-size: 12px">
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<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-menu>
</template>
</el-dropdown>
<span>欢迎您,{{ userStore.userInfo?.username }}</span>
<el-button type="text" @click="handleLogout" style="margin-left: 10px">退出</el-button>
</el-header>
<!-- 内容区域(路由出口) -->
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { useUserStore } from '@/pinia/user';
import { ElMessage } from 'element-plus';
// 引入Element Plus图标
import { House, List, Setting } from '@element-plus/icons-vue';
const router = useRouter();
const userStore = useUserStore();
// 菜单选择事件
const handleMenuSelect = (index: string) => {
switch (index) {
case '1':
router.push('/home');
break;
case '2':
router.push('/list');
break;
case '3-1':
ElMessage.info('用户管理功能开发中...');
break;
case '3-2':
ElMessage.info('角色管理功能开发中...');
break;
}
};
// 退出登录
const handleLogout = () => {
userStore.logout();
};
</script>
<style scoped lang="scss">
.el-header {
background-color: #fff;
color: #333;
line-height: 60px;
border-bottom: 1px solid #e6e6e6;
}
.el-aside {
color: #333;
.logo {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 16px;
border-bottom: 1px solid #404854;
img {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
}
}
}
.el-main {
background-color: #f5f7fa;
color: #333;
padding: 20px;
}
.el-menu-vertical-demo {
border-right: none;
height: calc(100vh - 60px);
}
</style>
步骤 2:修改首页组件(src/views/Home/index.vue)
html
<template>
<div class="home-content">
<el-card title="后台概览" class="home-card">
<el-row :gutter="20">
<el-col :span="6">
<div class="stat-card">
<p class="stat-title">今日访问量</p>
<p class="stat-value">1,234</p>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<p class="stat-title">今日订单数</p>
<p class="stat-value">567</p>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<p class="stat-title">今日销售额</p>
<p class="stat-value">¥89,012</p>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<p class="stat-title">用户总数</p>
<p class="stat-value">10,890</p>
</div>
</el-col>
</el-row>
</el-card>
<!-- 快速操作 -->
<el-card title="快速操作" class="home-card" style="margin-top: 20px;">
<el-button type="primary" icon="el-icon-plus">新增数据</el-button>
<el-button type="success" icon="el-icon-download">导出数据</el-button>
<el-button type="warning" icon="el-icon-upload">导入数据</el-button>
<el-button type="info" icon="el-icon-refresh">刷新数据</el-button>
</el-card>
</div>
</template>
<script setup lang="ts">
// 首页业务逻辑(可后续扩展)
</script>
<style scoped lang="scss">
.home-content {
width: 100%;
}
.home-card {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.stat-card {
background: linear-gradient(120deg, #e53935, #e35d5b);
padding: 20px;
border-radius: 8px;
color: #fff;
text-align: center;
.stat-title {
font-size: 14px;
margin-bottom: 10px;
opacity: 0.8;
}
.stat-value {
font-size: 24px;
font-weight: bold;
}
}
// 不同卡片不同渐变
.el-col:nth-child(2) .stat-card {
background: linear-gradient(120deg, #43a047, #4caf50);
}
.el-col:nth-child(3) .stat-card {
background: linear-gradient(120deg, #1e88e5, #2196f3);
}
.el-col:nth-child(4) .stat-card {
background: linear-gradient(120deg, #fdd835, #ffc107);
}
</style>
步骤 3:修改路由配置,让首页使用布局组件
修改src/router/index.ts中的首页路由:
javascript
{
path: '/home',
name: 'Home',
// 布局组件作为父组件,首页内容作为子路由
component: () => import('@/layouts/MainLayout.vue'),
children: [
{
path: '', // 子路由默认路径
component: () => import('@/views/Home/index.vue'),
},
],
meta: {
requiresAuth: true,
title: '后台首页',
},
},
{
path: '/list',
name: 'List',
component: () => import('@/layouts/MainLayout.vue'),
children: [
{
path: '',
component: () => import('@/views/List/index.vue'),
},
],
meta: {
requiresAuth: true,
title: '数据列表页',
},
},
六、实战踩坑与解决方案
6.1 常见问题及解决
| 问题场景 | 原因分析 | 解决方案 |
|---|---|---|
| 路由跳转白屏 | 路由懒加载路径错误,或布局组件无路由出口 | 1. 检查 import 路径是否正确;2. 确保布局组件中有<router-view /> |
| Pinia 持久化失效 | 未安装 pinia-plugin-persistedstate,或配置错误 | 1. 确认安装插件并注册;2. 检查 persist 配置中的 paths 是否包含需要持久化的字段 |
| Axios 请求跨域 | 开发环境未配置代理,或生产环境后端未配置 CORS | 1. Vite 配置 proxy 代理(见 vite.config.ts);2. 后端配置 Access-Control-Allow-Origin |
| Element Plus 图标不显示 | 未按需引入图标,或版本不兼容 | 1. 手动引入需要的图标(如 import {House} from '@element-plus/icons-vue');2. 确保 Element Plus 版本≥2.3.0 |
| 登录后刷新页面状态丢失 | Pinia 未开启持久化,或 token 未存储到 localStorage | 启用 pinia-plugin-persistedstate,配置 persist 存储到 localStorage |
6.2 性能优化建议
- 路由懒加载:所有页面均使用
() => import('@/views/xxx/index.vue')懒加载,减少首屏加载体积; - Element Plus 按需引入:通过 unplugin-vue-components 插件自动按需引入组件,避免全局引入体积过大;
- Axios 取消重复请求:添加取消请求逻辑,避免同一接口多次请求导致数据错乱;
- 图片懒加载:首页统计卡片的图片使用
v-lazy指令懒加载(需安装 vue3-lazy)。
七、总结与进阶建议
7.1 核心总结
- 路由管理:通过 Vue Router 实现多页面导航,结合路由守卫控制登录权限,参数传递支持 params/query 两种方式;
- 状态管理:Pinia 替代 Vuex,轻量且支持持久化,完美适配后台系统的用户状态管理;
- 请求封装:Axios 拦截器统一处理 token、错误、加载状态,封装通用请求方法提升开发效率;
- UI 开发:Element Plus 提供丰富的后台组件,结合布局组件快速搭建后台系统页面结构。
7.2 进阶建议
- 权限控制:基于 Pinia 的用户角色,实现路由 / 按钮级别的权限控制;
- 国际化:集成 vue-i18n 实现多语言切换,适配海外后台系统;
- 主题定制:基于 Element Plus 的主题变量,实现动态主题切换;
- 错误监控:集成 Sentry 捕获前端错误,提升系统稳定性;
- 打包优化:配置 Vite 的 build 选项,压缩代码、拆分 chunk,减少生产环境包体积。
最后
本文从 0 到 1 整合了 Vue3 生态的核心工具,实现了一个具备完整登录流程、多页面导航、状态管理、请求封装的后台管理系统核心模块。内容覆盖基础配置、核心逻辑、踩坑解决方案,既适合 Vue3 新手入门实战,也能为企业级后台项目开发提供参考。
如果对你有帮助,欢迎点赞 + 收藏 + 关注,后续会持续更新 Vue3 后台系统进阶内容(如权限控制、国际化、打包优化)。
如果有任何问题或不同见解,欢迎在评论区交流哦~
